From e317d79a219591cace22613f1bb98aa7b98feace Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 2 May 2026 02:22:20 +0900 Subject: [PATCH] S3M eff X; PT funk repeat --- TAUD_NOTE_EFFECTS.md | 42 ++++++++++++++++++++++++++---------------- mod2taud.py | 8 ++++++-- s3m2taud.py | 18 +++++++++++++----- terranmon.txt | 14 +++++++------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index 43a18cd..3329270 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -111,7 +111,7 @@ Opcodes are single base-36 digits (0-9, then A-Z); arguments are 16-bit hexadeci --- -## D — Volume slide (multiple forms) +## D $xy00 — Volume slide (multiple forms) D's 16-bit argument encodes four mutually exclusive modes using the top nibble and the following byte. All forms operate on the channel's current volume and clip to $00..$3F after each step. @@ -515,6 +515,26 @@ A tempo slide's memory slot is separate from the set-tempo path and is private t --- +## W $xy00 — Global volume slide + +**Plain.** Similar to `D $xy00`, but applies to the global volume. + +**Compatibility.** IT `Wxy` maps directly. + +**Implementation.** See effect D, apply to the global volume instead. + +--- + +## X $xx00 — Fine Set Panning + +**Plain.** **Unimplemented**. On IT, sets the panning position of the current channel, $00 being full-left and $FF being full-right. + +**Compatibility.** Convert to `S $80xx`. + +**Implementation.** Not applicable. + +--- + ## Y $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 $5x. @@ -539,16 +559,6 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr --- -## X $xx00 — Fine Set Panning - -**Plain.** **Unimplemented**. On IT, sets the panning position of the current channel, $00 being full-left and $FF being full-right. - -**Compatibility.** Convert directly into panning effect `0.$xx`, rounded down to nearest 6-bit value. - -**Implementation.** Not applicable. - ---- - ## 8 $xyzz — Bitcrusher **Plain.** Applies Bitcrusher to the current voice. @@ -690,7 +700,7 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning. **Plain.** Sets the channel pan to `$xx`, with $00 being full left and $FF being full right. $80 is centre. -**Compatibility.** ST3 `S8x` uses a 4-bit value. +**Compatibility.** IT `Xxx` maps directly. ST3 `S8x` uses a 4-bit value. 1. convert by nibble-repeat: ST3 `S83` → Taud `S $8033`. Panning column command `0.$xx` has the same semantics and is the preferred form when a pan column is available in the pattern. ProTracker `8xx` (fine pan) and `E8x` (coarse pan) both map into Taud's 8-bit pan — the ProTracker 8-bit form maps directly; the 4-bit form nibble-repeats. 2. convert to PanEff: ST3 `S8x` → PanEff `0.yy`, where `yy = round(4.2 * x)` @@ -762,19 +772,19 @@ Q retrigger counters do **not** reset between SEx repetitions. --- -## S $Fx00 — Funk repeat with speed $x (non-destructive) +## S $Fxxx — Funk repeat with speed $xxx (non-destructive) **Plain.** Produces a hiss-like progressive inversion of the sample loop, toggling individual bytes over time for a gritty textural effect. Setting `$x = 0` turns the effect off; higher `$x` advances the inversion faster. -**Compatibility.** ProTracker `EFx` is destructive — it XORs bytes directly in the sample data, permanently corrupting the sample. **Taud's implementation is non-destructive**: the XOR is applied at playback time through a per-instrument bit-mask, leaving source samples pristine. ST3 does not implement SFx at all and will parse Taud's S $Fx00 as a no-op; converters targeting ST3 should drop the effect. ProTracker `EFx` imports directly as Taud `S $Fx00`. +**Compatibility.** ProTracker `EFx` is destructive — it XORs bytes directly in the sample data, permanently corrupting the sample. **Taud's implementation is non-destructive**: the XOR is applied at playback time through a per-instrument bit-mask, leaving source samples pristine. ST3 does not implement SFx at all and will parse Taud's S $Fx00 as a no-op; converters targeting ST3 should drop the effect. ProTracker `EFx` imports as Taud `S $Fyyy`, where `yyy = funk_table[x]`. **Implementation.** Each instrument carries a `funk_mask` bit array, one bit per byte of the loop region, all zero at song start. A per-channel counter `funk_accumulator` and a per-channel `funk_write_pos` track progress. ``` funk_table[16] = { 0, 5, 6, 7, 8, $A, $B, $D, $10, $13, $16, $1A, $20, $2B, $40, $80 } -on every tick (when S $Fx00 is active with x != 0): - funk_accumulator += funk_table[x] +on every tick (when S $Fxxxx is active with x != 0): + funk_accumulator += funk_length while funk_accumulator >= $80: funk_accumulator -= $80 bit = funk_mask[funk_write_pos] diff --git a/mod2taud.py b/mod2taud.py index 63430e8..f3c8785 100644 --- a/mod2taud.py +++ b/mod2taud.py @@ -343,7 +343,8 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple: if sub == 0xE: return (TOP_S, 0xE000 | (x << 8), None, None) if sub == 0xF: - return (TOP_S, 0xF000 | (x << 8), None, None) + funk_table = [0, 5, 6, 7, 8, 0xA, 0xB, 0xD, 0x10, 0x13, 0x16, 0x1A, 0x20, 0x2B, 0x40, 0x80] + return (TOP_S, 0xF000 | funk_table[x]), None, None) return (TOP_NONE, 0, None, None) if cmd == 0xF: @@ -520,7 +521,10 @@ def build_sample_inst_bin(samples: list) -> tuple: struct.pack_into('> 4) & 0xF val = arg & 0xF - if sub in (0x1, 0x2, 0x3, 0x4, 0xB, 0xC, 0xD, 0xE, 0xF): + if sub in (0x1, 0x2, 0x3, 0x4, 0xB, 0xC, 0xD, 0xE): + vprint(f" dropped S{sub:01X} at ch{ch} row{row}") return (TOP_S, (sub << 12) | (val << 8), None, None) if sub == 0x5: # Panbrello LFO waveform — maps directly to Taud S$5x00. return (TOP_S, 0x5000 | (val << 8), None, None) if sub == 0x8: - # S8x → PanEff 0.yy where yy = round(x * 4.2), mapping nibble 0-15 to pan 0-63. - return (TOP_NONE, 0, None, (SEL_SET, round(val * 4.2))) + # S8x: 4-bit → nibble-repeat into 8-bit SEL_SET pan + pan8 = (val << 4) | val + return (TOP_S, 0x8000 | pan8, None, None) + if sub == 0xF: + funk_table = [0, 5, 6, 7, 8, 0xA, 0xB, 0xD, 0x10, 0x13, 0x16, 0x1A, 0x20, 0x2B, 0x40, 0x80] + return (TOP_S, 0xF000 | funk_table[x]), None, None) # S0/S6/S7/S9/SA: filter, NNA, sound-control, stereo — drop silently. return (TOP_NONE, 0, None, None) @@ -355,7 +360,7 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0, return (TOP_NONE, 0, None, None) if cmd == EFF_X: - return (TOP_NONE, 0, None, (SEL_SET, min(arg >> 2, 0x3F))) + return (TOP_S, 0x8000 | (arg & 0xFF), None, None) if cmd == EFF_Y: hi = (arg >> 4) & 0xF @@ -508,7 +513,10 @@ def build_sample_inst_bin(instruments: list) -> tuple: # Volume env point 0: hold at env_vol indefinitely (offset minifloat = 0 → hold). inst_bin[base + 21] = env_vol inst_bin[base + 22] = 0 - inst_bin[base + 171] = 0xFF # instrument global volume + inst_bin[base + 171] = 0xFF # instrument global volume + inst_bin[base + 176] = 0xFF # default pan = off + inst_bin[base + 181] = 0xFF # filter cutoff = off + inst_bin[base + 182] = 0xFF # filter resonance = off vprint(f" instrument[{base // 192}] '{inst.name}' ptr: '{ptr}', sampling rate: '{inst.c2spd}'") if inst.c2spd > 65535: diff --git a/terranmon.txt b/terranmon.txt index 174ede5..a2d74d5 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2026,7 +2026,7 @@ Instrument bin: Registry for 256 instruments, formatted as: b: use envelope c: envelope carry - p: use default pan + p: use default pan (see offset 176 "Default pan value" below) t: the loop must sustain (key-off escapes the loop) u: set to enable the sustain/loop @@ -2066,7 +2066,7 @@ Instrument bin: Registry for 256 instruments, formatted as: * 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) + Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag) * 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) @@ -2083,16 +2083,16 @@ Instrument bin: Registry for 256 instruments, formatted as: * 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 + Byte[4] Reserved TODO: - * implement Instrument Flag, Vibrato Depth, Vibrato Rate, other samplewise/instrumentwise changes to it2taud - * implement sample loop sustain + * implement Instrument Flag, Vibrato Depth, Vibrato Rate, other samplewise/instrumentwise changes to it2taud and audio engine * implement new note action on the audio engine (IT uses "background channels", maybe we can do the same but make "background channels" mixer-private) + * on playback, panning changes randomly on Taud made by s3m2taud.py and mod2taud.py * implement S6x and S7x command - * implement Vxx (set global volume) and Wxx command (global volume slide) + * implement Wxx command (global volume slide) + * implement sample loop sustain * 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