IT filters

This commit is contained in:
minjaesong
2026-05-01 23:19:49 +09:00
parent a4adc428d0
commit fe59df18f7
7 changed files with 186 additions and 73 deletions

View File

@@ -18,6 +18,26 @@ Documentation for TSVM and TVDOS are available on `./doc/*.tex` as machine-reada
Documentatino for TSVM architecture is available on `terranmon.txt`
## Reference Materials
Third-party source-code references that inform TSVM implementations live in
`reference_materials/<topic>/`. Each topic folder has a `README.md` that
summarises the takeaway and points back into the verbatim source files.
**Consult these before reimplementing tracker / codec / DSP behaviour from
memory** — TSVM aims to match the audible behaviour of the originals.
Current topics:
- `reference_materials/tracker_filter/` — Impulse Tracker / OpenMPT / Schism
Tracker resonant low-pass filter source. Defines the cutoff formula, the
resonance damping curve, and the **IIR-only 2-pole topology** (NOT a
biquad — no feedforward x[n1] / x[n2] terms) that `AudioAdapter.kt` uses
for Taud playback.
When fetching new references, copy the relevant upstream files verbatim into
a topic folder, write a `README.md` summarising the relevant maths /
algorithms with file:line citations, and add an entry here.
## Architecture
### Core Components

View File

@@ -648,6 +648,41 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
**Implementation.** As for S $3x, but applied to Y's separate state (`panbrello_waveform`, `panbrello_retrigger`, and panbrello `lfo_pos`).
---
## S $6x00 — Fine pattern delay
**Plain.** Extends the current row by $x ticks. If multiple S6x commands are on the same row, the sum of their parameters is used.
**Compatibility.** IT `S6x` maps directly.
**Implementation.** TODO
---
## S $7x00 — Note/Instrument actions
**Plain.** Performs following action to the note.
| $x | Operation | Description |
|---|---|---|
| $0 | Past Note Cut | Cuts all notes playing as a result of New Note Actions on the current channel |
| $1 | Past Note Off | Sends a Note Off to all notes playing as a result of New Note Actions on the current channel |
| $2 | Past Note Fade | Fades out all notes playing as a result of New Note Actions on the current channel |
| $3 | NNA Note Cut | Sets the currently active note's New Note Action to Note Cut |
| $4 | NNA Note Continue | Sets the currently active note's New Note Action to Continue |
| $5 | NNA Note Off | Sets the currently active note's New Note Action to Note Off |
| $6 | NNA Note Fade | Sets the currently active note's New Note Action to Note Fade |
| $7 | Volume Envelope Off | Disables the currently active note's volume envelope |
| $8 | Volume Envelope On | Enables the currently active note's volume envelope |
| $9 | Panning Envelope Off | Disables the currently active note's panning envelope |
| $A | Panning Envelope On | Enables the currently active note's panning envelope |
| $B | Pitch Envelope Off | Disables the currently active note's pitch or filter envelope |
| $C | Pitch Envelope On | Enables the currently active note's pitch envelope |
**Compatibility.** IT `S7x` maps directly.
**Implementation.** TODO
---

View File

@@ -2370,6 +2370,8 @@ taud.uploadTaudFile(fullPathObj.full, 0, PLAYHEAD)
audio.setMasterVolume(PLAYHEAD, 255)
audio.setMasterPan(PLAYHEAD, 128)
const initialTrackerMixerflags = audio.getTrackerMixerFlags(PLAYHEAD)
//const initialGlobalVolume =
//const initialMixingVolume =
function isExternalPanel(p) {
return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE

View File

@@ -507,8 +507,10 @@ def parse_instruments(data: bytes, h: ITHeader) -> list:
# Initial filter cutoff/resonance (high bit = enabled, low 7 bits = value)
ifc_raw = data[ptr + 0x39]
ifr_raw = data[ptr + 0x3A]
inst.ifc = ifc_raw & 0x7F if (ifc_raw & 0x80) else 0
inst.ifr = ifr_raw & 0x7F if (ifr_raw & 0x80) else 0
# Taud uses full 0..255 range (double IT's resolution): IT 0..127 → Taud 0..254,
# IT "off" (high bit clear) → Taud 255.
inst.ifc = (ifc_raw & 0x7F) * 2 if (ifc_raw & 0x80) else 255
inst.ifr = (ifr_raw & 0x7F) * 2 if (ifr_raw & 0x80) else 255
# Parse IT envelopes (new-format only, ≥cmwt 0x200)
# Vol envelope at ptr+0x130; pan envelope at ptr+0x182; pf envelope at ptr+0x1D4
@@ -1234,8 +1236,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
struct.pack_into('b', inst_bin, base + 180,
max(-128, min(127, idata.get('pps', 0))))
inst_bin[base + 181] = idata.get('pan_swing', 0) & 0xFF
inst_bin[base + 182] = idata.get('ifc', 0) & 0xFF
inst_bin[base + 183] = idata.get('ifr', 0) & 0xFF
inst_bin[base + 182] = idata.get('ifc', 255) & 0xFF
inst_bin[base + 183] = idata.get('ifr', 255) & 0xFF
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")

View File

@@ -2002,8 +2002,10 @@ Instrument bin: Registry for 256 instruments, formatted as:
Uint16 Loop Start (can be smaller than Play Start)
Uint16 Loop End
Bit8 Sample Flags
0b 0000 00pp
0b 0000 0spp
pp: loop mode. 0-no loop, 1-loop, 2-backandforth, 3-oneshot (ignores note length unless overridden by other notes)
s: loop is sustain (key-off escapes the loop)
- IT: look for sample's SusLoop flag
Bit16 Volume envelope sustain/loops and flags
* Sustain is implemented by enabling 't' flag. FastTracker has no 'Sus Loop' but only 'Sus Point'; use same value for start and end index
0b 0ut sssss pcb eeeee
@@ -2051,26 +2053,50 @@ Instrument bin: Registry for 256 instruments, formatted as:
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
Uint8 Instrument Global Volume (0..255)
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
- ImpulseTracker has samplewise default volume (0..64) and samplewise global volume (0..64), and they must be taken into account because Taud has no samplewise config, following the ImpulseTracker spec
* FastTracker2 has range of 0..64; multiply by (255/64) then round to int
Uint8 Volume Fadeout low bits (IT: 1..256; XM: 0..255)
Bit8 Fadeout and vibrato
0b dddd ffff
0b 0000 ffff
f: Volume Fadeout high bits
d: Vibrato depth
Uint8 Volume swing (0..255 full range)
Uint8 Vibrato speed
* ImpulseTracker has samplewise vibrato speed (0..64), and they must be taken into account because Taud has no samplewise config
* FastTracker2 has instrumentwise config (0..255)
* The spec follows FastTracker2, and conversion must be performed when importing from FastTracker2
Uint8 Vibrato sweep
* FastTracker2 instrument config
Uint8 Default pan value (0..255 full range)
* ImpulseTracker has samplewise default volume and samplewise global volume, and they must be taken into account because Taud has no samplewise config
Uint16 Pitch-pan centre (4096-TET note value)
Sint8 Pitch-pan separation (-128..127 full range)
Uint8 Pan swing (0..255 full range)
Uint8 Default cutoff (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud)
Uint8 Default resonance (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud)
Uint16 Sample detune (in 4096-TET unit) (XM finetune scale need to be rescaled accordingly)
Byte[6] Reserved
Bit8 Instrument Flag
0b 000 www nn
n: New note action. 00: note off, 01: note cut, 10: continue, 11: note fade (arranged differently to IT)
ww: Vibrato waveform (IT: sample config, FT2: instrument config). 00: sine, 01: ramp-down saw, 10: square, 11: random, 100: ramp-up saw (FT2 only)
Uint8 Vibrato Depth (0..255 full range)
* ImpulseTracker has range of 0..32 ON THE SAMPLE SETTINGS; multiply by (255/32) then round to int
* FastTracker2 has range of 0..16; multiply by (255/16) then round to int
Uint8 Vibrato Rate (0..255 full range)
* ImpulseTracker sample config. The spec follows ImpulseTracker precisely
Byte[3] Reserved
TODO:
* implement Instrument Flag, Vibrato Depth, Vibrato Rate, other samplewise/instrumentwise changes to it2taud
* implement sample loop sustain
* implement new note action on the audio engine (IT uses "background channels", maybe we can do the same but make "background channels" mixer-private)
* implement S6x and S7x command
* implement Vxx (set global volume) and Wxx command (global volume slide)
* Amiga mode freq shift now "underdelivers" (pitch bend not "strong" enough)
* cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
* figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
TODO: after *2taud.py is done, extend with 25 envelopes and add Pitch/Filter features. 192 bytes per instrument granted (48k space). This is a breaking change.
TODO: use it2taud.py to implement pitch/filter -- don't delete rerender code yet
Play Data: play data are series of tracker-like instructions, visualised as:
@@ -2371,8 +2397,8 @@ prefixes:
Note: custom notations will use internal index 65535 down to 65520 (index 0 = 65535, index 15 = 65520)
Note Tuning:
1. "Base Note at C3" will be derived using "Current Tuning Base Note" and "Frequency at the Base Note" from the song table. If the values are A3,440Hz, it will be converted to C3,261.6255653Hz
2. Frequency at C4 will be (Base Note at C3) × (Interval Size)
1. "Base Note at C4" will be derived using "Current Tuning Base Note" and "Frequency at the Base Note" from the song table. If the values are A4,440Hz, it will be converted to C4,261.6255653Hz
2. Frequency at C5 will be (Base Note at C4) × (Interval Size)
3. 4096 notes will be equidistance-distributed between (Frequency at C3) and (Frequency at C4), with logarithmic pitch progression; this builds the frequency-offset table
4. Frequency-Offset Table from the previous step will be applied against the "Base Note at C3" to construct the notes within the notation. Value at index zero of the Frequency Table must be 0
5. The progress will continue outside the "root interval" (C3..C4) to build a complete note-to-frequency table

View File

@@ -124,10 +124,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
internal val DBGPRN = false
const val SAMPLING_RATE = 32000
const val TRACKER_CHUNK = 512
const val TRACKER_C3 = 0x4000 // legacy alias (one octave below the new reference)
const val TRACKER_C4 = 0x5000 // reference C for instrument samplingRate (terranmon.txt:2000)
// Amiga period at TRACKER_C4 for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
// PT "C-2" period 428 ↔ TSVM TRACKER_C4 ↔ 8363 Hz; mod2taud uses the same convention.
const val MIDDLE_C = 0x5000 // reference C for instrument samplingRate (terranmon.txt:2000)
// Amiga period at MIDDLE_C for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
// PT "C-2" period 428 ↔ TSVM MIDDLE_C ↔ 8363 Hz; mod2taud uses the same convention.
// Trackers may use different labelling conventions (e.g. C5) for Middle C.
// For non-tracker context, Middle C shall be labelled as C4.
const val AMIGA_BASE_PERIOD = 428.0
}
@@ -1168,7 +1169,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double =
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C4) / 4096.0)
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - MIDDLE_C) / 4096.0)
// Applies one tick of Amiga-mode pitch slide. When the song is in Amiga tone mode, E/F coarse
// slide arguments are stored as raw tracker period units (the original ProTracker/ST3 byte),
@@ -1176,9 +1177,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// linear mode: negative = pitch down (E effect), positive = pitch up (F effect), so a positive
// slideArg subtracts from the period (pitch rises).
private fun amigaSlide(noteVal: Int, slideArg: Int): Int {
val period = AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - TRACKER_C4).toDouble() / 4096.0)
val period = AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - MIDDLE_C).toDouble() / 4096.0)
val newPeriod = (period - slideArg).coerceAtLeast(1.0)
return (TRACKER_C4 + 4096.0 * log2(AMIGA_BASE_PERIOD / newPeriod)).roundToInt()
return (MIDDLE_C + 4096.0 * log2(AMIGA_BASE_PERIOD / newPeriod)).roundToInt()
}
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
@@ -1318,11 +1319,28 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
/**
* Recompute the biquad LPF coefficients for `voice` when its cutoff or
* resonance has changed since the last refresh. Cutoff 0..255 maps
* exponentially from ~110 Hz to ~14 kHz; resonance 0..255 maps linearly
* to Q ∈ [0.5, 6.0]. The filter is disabled at full-open (cutoff ≥ 0xFE
* with no resonance), avoiding the per-sample cost when transparent.
* Recompute the IT-compatible 2-pole resonant low-pass coefficients for
* `voice` when its cutoff or resonance has changed since the last refresh.
*
* Taud's filter range mirrors Impulse Tracker's at double resolution:
* Taud 0..254 maps to IT 0..127, while Taud 255 means "filter off" (the
* IT high-bit-clear sentinel). The filter is bypassed when cutoff = 255.
*
* The coefficient math and topology mirror OpenMPT/Schism Tracker (see
* reference_materials/tracker_filter/openmpt_Snd_flt.cpp and
* schism_filters.c). Notably this is NOT a biquad: the recurrence has no
* feedforward x[n-1] / x[n-2] terms.
*
* frequency = 110 Hz × 2^(itCutoff / 24 + 0.25) (IT 0..127)
* dmpfac = 10 ^ (-itResonance × 0.009375) (= 24/128/20 dB)
* r = mixingFreq / (2π × frequency)
* d = dmpfac × r + dmpfac 1
* e = r²
* denom = 1 + d + e
* A0 = 1 / denom
* B0 = (d + 2e) / denom
* B1 = e / denom
* y[n] = A0 × x[n] + B0 × y[n1] + B1 × y[n2]
*/
private fun refreshVoiceFilter(voice: Voice) {
val cut = voice.currentCutoff.coerceIn(0, 255)
@@ -1331,49 +1349,42 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.filterCutoffCached = cut
voice.filterResonanceCached = res
if (cut >= 0xFE && res == 0) {
if (cut >= 255) {
voice.filterActive = false
return
}
// Exponential cutoff: 110 Hz × 2^(cut × log2(14000/110) / 255).
// log2(14000/110) ≈ 6.992, so exponent ≈ cut × 0.0274.
val cutoffHz = 110.0 * 2.0.pow(cut * 6.992 / 255.0)
val itCutoff = cut * 0.5 // 0..127
val itResonance = if (res >= 255) 0.0 else res * 0.5 // 0..127
val nyquist = SAMPLING_RATE * 0.5 - 1.0
val f0 = cutoffHz.coerceIn(20.0, nyquist)
val Q = 0.5 + (res / 255.0) * 5.5
val frequency = (110.0 * 2.0.pow(itCutoff / 24.0 + 0.25)).coerceAtMost(nyquist)
val dmpfac = 10.0.pow(-itResonance * (24.0 / 128.0) / 20.0)
val w0 = 2.0 * PI * f0 / SAMPLING_RATE
val cosW = cos(w0)
val sinW = sin(w0)
val alpha = sinW / (2.0 * Q)
val r = SAMPLING_RATE / (2.0 * PI * frequency)
val d = dmpfac * r + dmpfac - 1.0
val e = r * r
val denom = 1.0 + d + e
val b0 = (1.0 - cosW) * 0.5
val b1 = 1.0 - cosW
val b2 = b0
val a0 = 1.0 + alpha
val a1 = -2.0 * cosW
val a2 = 1.0 - alpha
voice.filterB0 = b0 / a0
voice.filterB1 = b1 / a0
voice.filterB2 = b2 / a0
voice.filterA1 = a1 / a0
voice.filterA2 = a2 / a0
voice.filterA0 = 1.0 / denom
voice.filterB0 = (d + e + e) / denom
voice.filterB1 = -e / denom
voice.filterActive = true
}
/** Apply the cached biquad LPF to one mono sample. Caller must have called
* refreshVoiceFilter at the start of the tick. */
/** Apply the cached IT-style 2-pole LPF to one mono sample. Caller must
* have called refreshVoiceFilter at the start of the tick. The history
* taps are clipped to ±2.0 to tame resonance ringing on extreme settings,
* matching OpenMPT's ClipFilter helper. */
private fun applyVoiceFilter(voice: Voice, x0: Double): Double {
if (!voice.filterActive) return x0
val y0 = voice.filterB0 * x0 +
voice.filterB1 * voice.filterX1 +
voice.filterB2 * voice.filterX2 -
voice.filterA1 * voice.filterY1 -
voice.filterA2 * voice.filterY2
voice.filterX2 = voice.filterX1; voice.filterX1 = x0
voice.filterY2 = voice.filterY1; voice.filterY1 = y0
val y1Clipped = voice.filterY1.coerceIn(-2.0, 2.0)
val y2Clipped = voice.filterY2.coerceIn(-2.0, 2.0)
val y0 = voice.filterA0 * x0 +
voice.filterB0 * y1Clipped +
voice.filterB1 * y2Clipped
voice.filterY2 = voice.filterY1
voice.filterY1 = y0
return y0
}
@@ -1499,9 +1510,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.rowPan = (voice.channelPan ushr 2).coerceIn(0, 63)
}
// Filter cutoff/resonance defaults — adjusted per-tick by the pf envelope when in filter mode.
voice.currentCutoff = if (inst.defaultCutoff > 0) inst.defaultCutoff else 0xFF
// 255 = filter off (IT high-bit-clear); 0..254 = active range matching IT 0..127 at double resolution.
voice.currentCutoff = inst.defaultCutoff
voice.currentResonance = inst.defaultResonance
voice.filterX1 = 0.0; voice.filterX2 = 0.0
voice.filterY1 = 0.0; voice.filterY2 = 0.0
voice.filterCutoffCached = -1 // force coefficient refresh on first tick
voice.filterResonanceCached = -1
@@ -1942,7 +1953,6 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.fadeoutVolume = 1.0
voice.autoVibPhase = 0
voice.autoVibTicksSinceTrigger = 0
voice.filterX1 = 0.0; voice.filterX2 = 0.0
voice.filterY1 = 0.0; voice.filterY2 = 0.0
voice.rowVolume = applyRetrigVolMod(voice.rowVolume, voice.retrigVolMod)
voice.channelVolume = voice.rowVolume
@@ -1962,9 +1972,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.playbackRate = computePlaybackRate(inst, finalPitch)
// Filter envelope (filter mode): scale current cutoff by env value (0..1, 0.5 = unity).
// If the instrument has no initial cutoff (255 = off), the envelope drives the filter
// from the maximum active value (254) so the filter can become audible during the note.
if (voice.hasPfEnv && voice.envPfIsFilter) {
val baseCut = if (inst.defaultCutoff > 0) inst.defaultCutoff else 0xFF
voice.currentCutoff = (baseCut * (voice.envPfValue * 2.0)).toInt().coerceIn(0, 0xFF)
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
voice.currentCutoff = (baseCut * (voice.envPfValue * 2.0)).toInt().coerceIn(0, 254)
}
// Refresh biquad filter coefficients once per tick (only recomputes when changed).
@@ -2264,19 +2276,18 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var autoVibPhase = 0 // 8-bit phase counter
var autoVibTicksSinceTrigger = 0 // for sweep ramp-up
// Filter / cutoff state — drives the per-voice 2-pole resonant LPF.
var currentCutoff = 0xFF // 0..255 (0xFF = open / unfiltered)
var currentResonance = 0 // 0..255
// Biquad state (updated per output sample) and cached coefficients
// (recomputed per tick when cutoff/resonance change).
// Filter / cutoff state — drives the per-voice IT-compatible 2-pole resonant LPF.
// Convention: 255 = filter off (matches IT's high-bit-clear sentinel);
// 0..254 = active range mirroring IT 0..127 at double resolution.
var currentCutoff = 0xFF
var currentResonance = 0xFF
// IT 2-pole IIR-only state (updated per output sample) and cached coefficients
// (recomputed per tick when cutoff/resonance change). Recurrence:
// y[n] = A0 × x[n] + B0 × y[n-1] + B1 × y[n-2]
var filterActive = false
var filterB0 = 1.0
var filterA0 = 1.0
var filterB0 = 0.0
var filterB1 = 0.0
var filterB2 = 0.0
var filterA1 = 0.0
var filterA2 = 0.0
var filterX1 = 0.0
var filterX2 = 0.0
var filterY1 = 0.0
var filterY2 = 0.0
// Snapshot of cutoff/resonance the cached coefficients correspond to.
@@ -2591,7 +2602,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
* Layout:
* 0..3 u32 sample pointer
* 4..5 u16 sample length
* 6..7 u16 sampling rate at TRACKER_C4 (0x5000)
* 6..7 u16 sampling rate at Middle C (0x5000) // NOTE: Taud treats middle C as C4, but some trackers show you C4 even if they are internally C5. Best practice: copy the value as-is.
* 8..9 u16 play start
* 10..11 u16 loop start
* 12..13 u16 loop end
@@ -2621,7 +2632,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var samplePtr: Int, // 32-bit sample bin offset
var sampleLength: Int,
var samplingRate: Int, // rate at TRACKER_C4
var samplingRate: Int, // rate at MIDDLE_C
var samplePlayStart: Int,
var sampleLoopStart: Int,
var sampleLoopEnd: Int,

View File

@@ -2,6 +2,7 @@ package net.torvald.tsvm
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.*
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion
@@ -205,6 +206,8 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
private var updateAkku = 0.0
private var updateRate = 1f / 60f
private var crtShaderSignalMode = 0
override fun render() {
gdxClearAndSetBlend(.094f, .094f, .094f, 0f)
setCameraPosition(0f, 0f)
@@ -250,6 +253,19 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
vm.update(delta)
if (vm.resetDown) rebootRequested = true
if ((Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT)) &&
(Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) || Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT))) {
if (Gdx.input.isKeyPressed(Input.Keys.S)) {
crtShaderSignalMode = -1 // RGB
}
else if (Gdx.input.isKeyPressed(Input.Keys.D)) {
crtShaderSignalMode = 0 // S-video
}
else if (Gdx.input.isKeyPressed(Input.Keys.F)) {
crtShaderSignalMode = 1 // Composite
}
}
}
fun poke(addr: Long, value: Byte) = vm.poke(addr, value)
@@ -292,6 +308,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
batch.shader.setUniformf("resolution", viewportWidth.toFloat(), viewportHeight.toFloat())
batch.shader.setUniformf("interlacer", (framecount % 2).toFloat())
batch.shader.setUniformf("time", (framecount % 640).toFloat())
batch.shader.setUniformi("signalMode", crtShaderSignalMode)
batch.setBlendFunctionSeparate(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA, GL20.GL_SRC_ALPHA, GL20.GL_ONE)
batch.draw(gpuFBO.colorBufferTexture, 0f, 0f)
}