From 3d99568359bee21c8492a4a2e54fbb489e80dca9 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Fri, 24 Apr 2026 09:15:24 +0900 Subject: [PATCH] taud: implemented eff W (panbrello) --- TAUD_NOTE_EFFECTS.md | 37 ++++++++++++++++++- assets/disk0/tvdos/bin/taut.js | 2 +- .../torvald/tsvm/peripheral/AudioAdapter.kt | 33 ++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index d025f3d..8e099c6 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -1,6 +1,6 @@ # Taud Tracker Effect Command Reference -Taud is a tracker-style music format derived from ScreamTracker 3's pattern command set, extended to 16-bit effect arguments and a 4096-tone equal-temperament pitch grid. This document defines every effect command a Taud engine must implement. Each command entry has three parts: a plain explanation for composers, compatibility notes for converting patterns from ScreamTracker 3 (ST3) or ProTracker (PT), and implementation details for engine writers. +Taud is a tracker-style music format derived from ScreamTracker 3's pattern command set, extended to 16-bit effect arguments and a 4096-tone equal-temperament pitch grid. This document defines every effect command a Taud engine must implement. Each command entry has three parts: a plain explanation for composers, compatibility notes for converting patterns from ScreamTracker 3 (ST3), ImpulseTracker (IT) or ProTracker (PT), and implementation details for engine writers. --- @@ -493,6 +493,30 @@ A tempo slide's memory slot is separate from the set-tempo path and is private t --- +## W $xxyy — Panbrello (panning vibrato) with speed $xx and depth $yy + +**Plain.** Modulates panning with an LFO, symmetrically with H's pitch modulation. `$xx` is LFO speed, `$yy` depth; the waveform is selected by S $4x. + +**Compatibility.** IT `Wxy` uses nibbles; convert by nibble-repeat. IT's volume cap is $40; Taud's is $3F — very deep vibrato that would have briefly clipped at $40 in IT may clip slightly earlier in Taud. W has its own memory slot. + +**Implementation.** Identical machinery to H with a larger shift to fit the narrower volume range: + +``` +on row parse (W): + if (arg >> 8) != 0: memory_W.speed = arg >> 8 + if (arg & $FF) != 0: memory_W.depth = arg & $FF + +on every tick (including tick 0): + sine = ModSinusTable[(lfo_pos >> 2) & $3F] + vol_delta = (sine × memory_W.depth) >> 9 + applied_vol = clamp(base_vol + vol_delta, 0, $3F) + lfo_pos = (lfo_pos + memory_W.speed × 4) & $FF +``` + +Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retrigger behaviour tracks the S $4x waveform nibble bit 2: cleared means retrigger on new note, set means preserve LFO position. + +--- + # The S subcommand family S is a multiplexing opcode; the **high nibble of the high byte** selects the sub-effect, and the remainder is the sub-argument. @@ -567,6 +591,17 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning. **Implementation.** As for S $3x, but applied to R's separate state (`tremolo_waveform`, `tremolo_retrigger`, and tremolo `lfo_pos`). +--- + +## S $5x00 — Panbrello LFO waveform + +**Plain.** Selects the shape of the panbrello (W) oscillator; value encoding is identical to S $3x. + +**Compatibility.** IT `S5x` maps directly. + +**Implementation.** As for S $3x, but applied to W's separate state (`panbrello_waveform`, `panbrello_retrigger`, and panbrello `lfo_pos`). + + --- ## S $80xx — Set channel pan position diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index ee3e819..43a571f 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -174,7 +174,7 @@ const colEffOp = 213 const colEffArg = 231 const colBackPtn = 255 -const PITCH_PRESET_IDX = 10123 // TODO read from the Project Data section of the .taud +const PITCH_PRESET_IDX = 240 // TODO read from the Project Data section of the .taud Number.prototype.hex02 = function() { return this.toString(16).toUpperCase().padStart(2,'0') diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 4380006..c2e5c2d 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -1088,7 +1088,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // H=0x11 vibrato, I=0x12 tremor, J=0x13 arpeggio, // K=0x14 K, L=0x15 L, O=0x18 sample offset, // Q=0x1A retrig, R=0x1B tremolo, S=0x1C subcommands, - // T=0x1D tempo, U=0x1E fine vibrato, V=0x1F global vol). + // T=0x1D tempo, U=0x1E fine vibrato, V=0x1F global vol, + // W=0x20 panbrello). // K (0x14) and L (0x15) are intentionally no-op in the engine — the // converter is required to split them into a recall-only H/G plus a // volume-column slide cell. @@ -1152,6 +1153,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { const val OP_T = 0x1D const val OP_U = 0x1E const val OP_V = 0x1F + const val OP_W = 0x20 } private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double = @@ -1241,9 +1243,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { } voice.rowVolume = voice.channelVolume voice.noteWasCut = false - // Vibrato/tremolo retrigger: reset LFO position when waveform requests it. + // Vibrato/tremolo/panbrello retrigger: reset LFO position when waveform requests it. if (voice.vibratoRetrig) voice.vibratoLfoPos = 0 if (voice.tremoloRetrig) voice.tremoloLfoPos = 0 + if (voice.panbrelloRetrig) voice.panbrelloLfoPos = 0 } private fun applyVolColumn(voice: Voice, value: Int, sel: Int) { @@ -1301,6 +1304,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { voice.tremorOn = 0 voice.vibratoActive = false voice.tremoloActive = false + voice.panbrelloActive = false voice.retrigActive = false voice.tempoSlideDir = 0 voice.volColSlideUp = 0; voice.volColSlideDown = 0 @@ -1471,6 +1475,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { val hi = (rawArg ushr 8) and 0xFF playhead.globalVolume = hi } + EffectOp.OP_W -> { + val sp = (rawArg ushr 8) and 0xFF + val dp = rawArg and 0xFF + if (sp != 0) voice.mem.wSpeed = sp + if (dp != 0) voice.mem.wDepth = dp + voice.panbrelloActive = true + } } } @@ -1486,6 +1497,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { } 0x3 -> { voice.vibratoWave = x and 3; voice.vibratoRetrig = (x and 4) == 0 } 0x4 -> { voice.tremoloWave = x and 3; voice.tremoloRetrig = (x and 4) == 0 } + 0x5 -> { voice.panbrelloWave = x and 3; voice.panbrelloRetrig = (x and 4) == 0 } 0x8 -> { // S$80xx — full 8-bit pan; arg low byte is the value. voice.channelPan = arg and 0xFF @@ -1609,6 +1621,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { voice.tremoloLfoPos = (voice.tremoloLfoPos + voice.mem.rSpeed * 4) and 0xFF } + // Panbrello (W) — modulates panning around base. + if (voice.panbrelloActive) { + val sine = lfoSample(voice.panbrelloLfoPos, voice.panbrelloWave) + val panDelta = (sine * voice.mem.wDepth) shr 9 + voice.rowPan = ((voice.channelPan ushr 2) + panDelta).coerceIn(0, 0x3F) + voice.panbrelloLfoPos = (voice.panbrelloLfoPos + voice.mem.wSpeed * 4) and 0xFF + } + // Arpeggio (J) — overrides pitchToMixer for this tick (overlay on basePitch). if (voice.arpActive) { val voiceIdx = ts.tickInRow % 3 @@ -1846,6 +1866,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // R (tremolo) — private speed and depth. var rSpeed: Int = 0 var rDepth: Int = 0 + // W (panbrello) — private speed and depth. + var wSpeed: Int = 0 + var wDepth: Int = 0 // Private slots var d: Int = 0 var i: Int = 0 @@ -1907,6 +1930,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { var tremoloWave = 0 var tremoloRetrig = true + // Panbrello (W) — uses memW. + var panbrelloActive = false + var panbrelloLfoPos = 0 + var panbrelloWave = 0 + var panbrelloRetrig = true + // Glissando flag (S$1x). var glissandoOn = false