mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
S3M eff X; PT funk repeat
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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('<H', inst_bin, base + 19, 0)
|
||||
inst_bin[base + 21] = env_vol
|
||||
inst_bin[base + 22] = 0
|
||||
inst_bin[base + 171] = 0xFF
|
||||
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[{taud_idx}] '{s.name}' ptr={ptr} c2spd={s.c2spd} "
|
||||
f"vol={s.volume} loop=({ls},{le},{'on' if loop_mode else 'off'})")
|
||||
|
||||
18
s3m2taud.py
18
s3m2taud.py
@@ -330,14 +330,19 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
||||
if cmd == EFF_S:
|
||||
sub = (arg >> 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user