mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
impl S6x and Wxx cmd
This commit is contained in:
@@ -666,7 +666,17 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
|
|||||||
|
|
||||||
**Compatibility.** IT `S6x` maps directly.
|
**Compatibility.** IT `S6x` maps directly.
|
||||||
|
|
||||||
**Implementation.** TODO
|
**Implementation.** Maintain a per-row accumulator `fine_delay_extra` on the tracker state, initialised to 0 at the start of every row parse (including pattern-delay repetitions caused by S $Ex). Each S $6x command encountered during the row scan adds `$x` to `fine_delay_extra`. The row then runs for `speed + fine_delay_extra` ticks instead of the usual `speed` ticks before advancing to the next row.
|
||||||
|
|
||||||
|
```
|
||||||
|
on row parse (S $6x):
|
||||||
|
fine_delay_extra += x # sum across all channels
|
||||||
|
|
||||||
|
row ends when:
|
||||||
|
tick_in_row >= ticks_per_row + fine_delay_extra
|
||||||
|
```
|
||||||
|
|
||||||
|
S $6x and S $Ex are orthogonal: when S $Ex is active the current row repeats `$x` additional times, and each repetition is itself extended by `fine_delay_extra` (re-accumulated from the same row's S $6x commands). There is no memory for S $6x; `$x == 0` is a no-op.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
32
it2taud.py
32
it2taud.py
@@ -25,7 +25,8 @@ Effect support:
|
|||||||
A-Z dispatch per TAUD_NOTE_EFFECTS.md. IT-specific: Cxx is binary
|
A-Z dispatch per TAUD_NOTE_EFFECTS.md. IT-specific: Cxx is binary
|
||||||
(not BCD like ST3). V scales by ×2 (IT 0-128 → Taud 0-255). X is
|
(not BCD like ST3). V scales by ×2 (IT 0-128 → Taud 0-255). X is
|
||||||
the full 8-bit IT pan. Y panbrello nibble-repeats. Z (MIDI macro)
|
the full 8-bit IT pan. Y panbrello nibble-repeats. Z (MIDI macro)
|
||||||
dropped. S6x tick-delay dropped. SAx high-offset dropped. S7x NNA /
|
dropped. S6x fine-pattern-delay forwarded directly to Taud S$6x. SAx
|
||||||
|
high-offset dropped. S7x NNA /
|
||||||
past-note / envelope toggles forwarded directly (IT sub-codes match
|
past-note / envelope toggles forwarded directly (IT sub-codes match
|
||||||
Taud one-to-one). Vol-column pitch-slide / tone-porta / vibrato sub-
|
Taud one-to-one). Vol-column pitch-slide / tone-porta / vibrato sub-
|
||||||
commands forwarded to main effect slot when empty; dropped otherwise.
|
commands forwarded to main effect slot when empty; dropped otherwise.
|
||||||
@@ -45,7 +46,7 @@ from taud_common import (
|
|||||||
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
||||||
NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4,
|
NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4,
|
||||||
TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I,
|
TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I,
|
||||||
TOP_J, TOP_K, TOP_L, TOP_O, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_Y,
|
TOP_J, TOP_K, TOP_L, TOP_O, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_W, TOP_Y,
|
||||||
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
|
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
|
||||||
EFF_A, EFF_B, EFF_C, EFF_D, EFF_E, EFF_F, EFF_G, EFF_H, EFF_I, EFF_J,
|
EFF_A, EFF_B, EFF_C, EFF_D, EFF_E, EFF_F, EFF_G, EFF_H, EFF_I, EFF_J,
|
||||||
EFF_K, EFF_L, EFF_M, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R, EFF_S, EFF_T,
|
EFF_K, EFF_L, EFF_M, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R, EFF_S, EFF_T,
|
||||||
@@ -104,7 +105,9 @@ VC_TPORTA_TABLE = (0, 1, 4, 8, 16, 32, 64, 96, 128, 255)
|
|||||||
IT_MEM_EFFECTS = frozenset({
|
IT_MEM_EFFECTS = frozenset({
|
||||||
EFF_D, EFF_E, EFF_F, EFF_G, EFF_H, EFF_I, EFF_J,
|
EFF_D, EFF_E, EFF_F, EFF_G, EFF_H, EFF_I, EFF_J,
|
||||||
EFF_K, EFF_L, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R,
|
EFF_K, EFF_L, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R,
|
||||||
EFF_S, EFF_T, EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y,
|
EFF_S, EFF_T, EFF_U, EFF_X, EFF_Y,
|
||||||
|
# EFF_V excluded: V00 means literal 0 in IT, not recall.
|
||||||
|
# EFF_W excluded: Taud engine handles W recall natively (same private-slot semantics).
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -784,7 +787,7 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
|||||||
- Cxx: binary row number, not BCD
|
- Cxx: binary row number, not BCD
|
||||||
- V: IT global vol 0-128 scaled ×2
|
- V: IT global vol 0-128 scaled ×2
|
||||||
- X: IT full 8-bit pan → 6-bit
|
- X: IT full 8-bit pan → 6-bit
|
||||||
- S6x, S7x, SAx, SFx handled (mostly dropped)
|
- S6x: fine pattern delay forwarded; S7x forwarded; SAx/SFx dropped
|
||||||
|
|
||||||
amiga_mode mirrors the inverse of the IT ``linear_slides`` flag. When
|
amiga_mode mirrors the inverse of the IT ``linear_slides`` flag. When
|
||||||
set, E/F coarse pitch-slide arguments are emitted as raw IT period units
|
set, E/F coarse pitch-slide arguments are emitted as raw IT period units
|
||||||
@@ -877,8 +880,8 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
|||||||
pan8 = (val << 4) | val
|
pan8 = (val << 4) | val
|
||||||
return (TOP_S, 0x8000 | pan8, None, None)
|
return (TOP_S, 0x8000 | pan8, None, None)
|
||||||
if sub == 0x6:
|
if sub == 0x6:
|
||||||
vprint(f" dropped S6{val:X} (tick delay) at ch{ch} row{row}")
|
# IT S6x = fine pattern delay (extends row by x ticks) — maps directly.
|
||||||
return (TOP_NONE, 0, None, None)
|
return (TOP_S, 0x6000 | (val << 8), None, None)
|
||||||
if sub == 0x7:
|
if sub == 0x7:
|
||||||
# NNA / past-note / envelope on-off — IT S7x maps directly to Taud S $7x00
|
# NNA / past-note / envelope on-off — IT S7x maps directly to Taud S $7x00
|
||||||
# (same sub-code table). No payload to translate.
|
# (same sub-code table). No payload to translate.
|
||||||
@@ -904,8 +907,8 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
|||||||
return (TOP_V, (taud_v & 0xFF) << 8, None, None)
|
return (TOP_V, (taud_v & 0xFF) << 8, None, None)
|
||||||
|
|
||||||
if cmd == EFF_W:
|
if cmd == EFF_W:
|
||||||
vprint(f" dropped W{arg:02X} (global vol slide) at ch{ch} row{row}")
|
# W$xy: same nibble-pair layout as D, passed in the high byte.
|
||||||
return (TOP_NONE, 0, None, None)
|
return (TOP_W, (arg & 0xFF) << 8, None, None)
|
||||||
|
|
||||||
if cmd == EFF_X:
|
if cmd == EFF_X:
|
||||||
return (TOP_S, 0x8000 | (arg & 0xFF), None, None)
|
return (TOP_S, 0x8000 | (arg & 0xFF), None, None)
|
||||||
@@ -926,8 +929,7 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
|||||||
|
|
||||||
def resolve_it_recalls(patterns_rows: list, order_list: list,
|
def resolve_it_recalls(patterns_rows: list, order_list: list,
|
||||||
num_channels: int, link_gef: bool,
|
num_channels: int, link_gef: bool,
|
||||||
old_effects: bool = False,
|
old_effects: bool = False) -> None:
|
||||||
initial_global_vol: int = 128) -> None:
|
|
||||||
"""Walk in order, resolve zero-arg recalls per-effect-per-channel.
|
"""Walk in order, resolve zero-arg recalls per-effect-per-channel.
|
||||||
|
|
||||||
IT effect memory groups:
|
IT effect memory groups:
|
||||||
@@ -940,14 +942,13 @@ def resolve_it_recalls(patterns_rows: list, order_list: list,
|
|||||||
they do NOT recall and are suppressed to TOP_NONE. All other effects
|
they do NOT recall and are suppressed to TOP_NONE. All other effects
|
||||||
still recall normally even in old_effects mode.
|
still recall normally even in old_effects mode.
|
||||||
|
|
||||||
V memory is primed with initial_global_vol so a song-leading V $0000
|
V and W are excluded from IT_MEM_EFFECTS and are not resolved here:
|
||||||
resolves to the header's global volume, not literal zero.
|
V00 in IT means literal 0 (not recall); W recall is handled natively
|
||||||
|
by the Taud engine's private W memory slot.
|
||||||
"""
|
"""
|
||||||
# last_mem[ch][eff_key] = last_non_zero_arg
|
# last_mem[ch][eff_key] = last_non_zero_arg
|
||||||
# eff_key: integer 1-26 for most effects; we merge cohorts by normalising.
|
# eff_key: integer 1-26 for most effects; we merge cohorts by normalising.
|
||||||
last_mem = [{} for _ in range(num_channels)]
|
last_mem = [{} for _ in range(num_channels)]
|
||||||
for ch in range(num_channels):
|
|
||||||
last_mem[ch][EFF_V] = initial_global_vol
|
|
||||||
|
|
||||||
# Effects that stop rather than recall when arg=0 in old_effects mode (ST3 compat).
|
# Effects that stop rather than recall when arg=0 in old_effects mode (ST3 compat).
|
||||||
# E/F: pitch slide stop. J: arpeggio stop (J00 = return to normal pitch in ST3).
|
# E/F: pitch slide stop. J: arpeggio stop (J00 = return to normal pitch in ST3).
|
||||||
@@ -1487,8 +1488,7 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
# ── Resolve IT recalls ───────────────────────────────────────────────────
|
# ── Resolve IT recalls ───────────────────────────────────────────────────
|
||||||
vprint(" resolving IT recalls…")
|
vprint(" resolving IT recalls…")
|
||||||
resolve_it_recalls(patterns_rows, h.order_list, 64, h.link_gef,
|
resolve_it_recalls(patterns_rows, h.order_list, 64, h.link_gef,
|
||||||
old_effects=h.old_effects,
|
old_effects=h.old_effects)
|
||||||
initial_global_vol=h.global_vol)
|
|
||||||
|
|
||||||
init_speed, _ = find_initial_bpm_speed(patterns_rows, h.order_list,
|
init_speed, _ = find_initial_bpm_speed(patterns_rows, h.order_list,
|
||||||
h.initial_speed, h.initial_tempo)
|
h.initial_speed, h.initial_tempo)
|
||||||
|
|||||||
11
s3m2taud.py
11
s3m2taud.py
@@ -17,8 +17,9 @@ Effect support:
|
|||||||
table" and "ScreamTracker 3 conversion notes". ST3 shared-memory recalls
|
table" and "ScreamTracker 3 conversion notes". ST3 shared-memory recalls
|
||||||
(D/E/F/I/J/K/L/Q/R/S with $00 arg) are eagerly resolved per channel.
|
(D/E/F/I/J/K/L/Q/R/S with $00 arg) are eagerly resolved per channel.
|
||||||
Cxx is BCD-decoded. K/L are split into H $0000 / G $0000 + volume-column
|
Cxx is BCD-decoded. K/L are split into H $0000 / G $0000 + volume-column
|
||||||
slide. M/N/X/P fold into volume / pan columns. W (global vol slide) is
|
slide. M/N/X/P fold into volume / pan columns. W (global vol slide)
|
||||||
dropped with a -v warning. X converts to pan column. Y (panbrello) converts
|
converts to Taud W (arg in high byte, same encoding as D). X converts to
|
||||||
|
pan column. Y (panbrello) converts
|
||||||
to Taud Y. S5 selects the panbrello LFO waveform. S8x converts to a pan
|
to Taud Y. S5 selects the panbrello LFO waveform. S8x converts to a pan
|
||||||
column SET of round(x * 4.2), mapping nibble 0-15 directly to pan 0-63.
|
column SET of round(x * 4.2), mapping nibble 0-15 directly to pan 0-63.
|
||||||
"""
|
"""
|
||||||
@@ -36,7 +37,7 @@ from taud_common import (
|
|||||||
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
||||||
NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4,
|
NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4,
|
||||||
TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I,
|
TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I,
|
||||||
TOP_J, TOP_K, TOP_L, TOP_O, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_Y,
|
TOP_J, TOP_K, TOP_L, TOP_O, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_W, TOP_Y,
|
||||||
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
|
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
|
||||||
EFF_A, EFF_B, EFF_C, EFF_D, EFF_E, EFF_F, EFF_G, EFF_H, EFF_I, EFF_J,
|
EFF_A, EFF_B, EFF_C, EFF_D, EFF_E, EFF_F, EFF_G, EFF_H, EFF_I, EFF_J,
|
||||||
EFF_K, EFF_L, EFF_M, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R, EFF_S, EFF_T,
|
EFF_K, EFF_L, EFF_M, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R, EFF_S, EFF_T,
|
||||||
@@ -356,8 +357,8 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
|||||||
return (TOP_V, (min(arg * 4, 0xFF) & 0xFF) << 8, None, None)
|
return (TOP_V, (min(arg * 4, 0xFF) & 0xFF) << 8, None, None)
|
||||||
|
|
||||||
if cmd == EFF_W:
|
if cmd == EFF_W:
|
||||||
vprint(f" dropped W{arg:02X} (global vol slide) at ch{ch} row{row}")
|
# W$xy: same nibble-pair layout as D, passed in the high byte.
|
||||||
return (TOP_NONE, 0, None, None)
|
return (TOP_W, (arg & 0xFF) << 8, None, None)
|
||||||
|
|
||||||
if cmd == EFF_X:
|
if cmd == EFF_X:
|
||||||
return (TOP_S, 0x8000 | (arg & 0xFF), None, None)
|
return (TOP_S, 0x8000 | (arg & 0xFF), None, None)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ TOP_S = 0x1C
|
|||||||
TOP_T = 0x1D
|
TOP_T = 0x1D
|
||||||
TOP_U = 0x1E
|
TOP_U = 0x1E
|
||||||
TOP_V = 0x1F
|
TOP_V = 0x1F
|
||||||
|
TOP_W = 0x20
|
||||||
TOP_Y = 0x22
|
TOP_Y = 0x22
|
||||||
|
|
||||||
# Volume / pan column selectors (2-bit field at top of vol/pan byte)
|
# Volume / pan column selectors (2-bit field at top of vol/pan byte)
|
||||||
|
|||||||
@@ -2093,8 +2093,8 @@ TODO:
|
|||||||
[x] on playback, panning changes randomly on Taud made by s3m2taud.py and mod2taud.py, but not by it2taud.py (maybe something's off with the instrument exports?)
|
[x] on playback, panning changes randomly on Taud made by s3m2taud.py and mod2taud.py, but not by it2taud.py (maybe something's off with the instrument exports?)
|
||||||
[x] NNA not disabled for S3M and MOD
|
[x] NNA not disabled for S3M and MOD
|
||||||
[x] `S B000` and `S B100` not working as intended -- on first playback it jumps to the next cue same row, on subsequent playbacks the commands are completely ignored
|
[x] `S B000` and `S B100` not working as intended -- on first playback it jumps to the next cue same row, on subsequent playbacks the commands are completely ignored
|
||||||
[ ] implement S6x command
|
[x] implement S6x command
|
||||||
[ ] implement Wxx command (global volume slide)
|
[x] implement Wxx command (global volume slide)
|
||||||
[ ] implement sample loop sustain
|
[ ] implement sample loop sustain
|
||||||
[ ] cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
[ ] 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
|
[ ] figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
||||||
|
|||||||
@@ -1679,6 +1679,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val cue = cueSheet[ts.cuePos]
|
val cue = cueSheet[ts.cuePos]
|
||||||
// Reset row-scope state before scanning channels.
|
// Reset row-scope state before scanning channels.
|
||||||
if (!ts.patternDelayActive) ts.sexWinningChannel = -1
|
if (!ts.patternDelayActive) ts.sexWinningChannel = -1
|
||||||
|
ts.finePatternDelayExtra = 0
|
||||||
|
|
||||||
for (vi in 0..19) {
|
for (vi in 0..19) {
|
||||||
val patNum = cue.patterns[vi]
|
val patNum = cue.patterns[vi]
|
||||||
@@ -1699,6 +1700,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
voice.panbrelloActive = false
|
voice.panbrelloActive = false
|
||||||
voice.retrigActive = false
|
voice.retrigActive = false
|
||||||
voice.tempoSlideDir = 0
|
voice.tempoSlideDir = 0
|
||||||
|
voice.wSlideDir = 0
|
||||||
voice.volColSlideUp = 0; voice.volColSlideDown = 0
|
voice.volColSlideUp = 0; voice.volColSlideDown = 0
|
||||||
voice.panColSlideRight = 0; voice.panColSlideLeft = 0
|
voice.panColSlideRight = 0; voice.panColSlideLeft = 0
|
||||||
voice.rowEffect = row.effect
|
voice.rowEffect = row.effect
|
||||||
@@ -1885,6 +1887,19 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val hi = (rawArg ushr 8) and 0xFF
|
val hi = (rawArg ushr 8) and 0xFF
|
||||||
playhead.globalVolume = hi
|
playhead.globalVolume = hi
|
||||||
}
|
}
|
||||||
|
EffectOp.OP_W -> {
|
||||||
|
val arg = resolveArg(rawArg, voice.mem.w).also { if (rawArg != 0) voice.mem.w = it }
|
||||||
|
val hi = (arg ushr 8) and 0xFF
|
||||||
|
val lo = hi and 0x0F
|
||||||
|
val hin = (hi ushr 4) and 0x0F
|
||||||
|
when {
|
||||||
|
hi == 0xFF -> playhead.globalVolume = (playhead.globalVolume + 0xF).coerceAtMost(0xFF) // WFF quirk: fine up by F
|
||||||
|
hin == 0xF && lo != 0 -> playhead.globalVolume = (playhead.globalVolume - lo).coerceAtLeast(0) // fine down on tick 0
|
||||||
|
lo == 0xF && hin != 0 -> playhead.globalVolume = (playhead.globalVolume + hin).coerceAtMost(0xFF) // fine up on tick 0
|
||||||
|
hin == 0 && lo != 0 -> { voice.wSlideDir = -1; voice.wSlideAmount = lo } // coarse down per non-first tick
|
||||||
|
lo == 0 && hin != 0 -> { voice.wSlideDir = +1; voice.wSlideAmount = hin } // coarse up per non-first tick
|
||||||
|
}
|
||||||
|
}
|
||||||
EffectOp.OP_Y -> {
|
EffectOp.OP_Y -> {
|
||||||
val sp = (rawArg ushr 8) and 0xFF
|
val sp = (rawArg ushr 8) and 0xFF
|
||||||
val dp = rawArg and 0xFF
|
val dp = rawArg and 0xFF
|
||||||
@@ -1908,6 +1923,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
0x3 -> { voice.vibratoWave = x and 3; voice.vibratoRetrig = (x and 4) == 0 }
|
0x3 -> { voice.vibratoWave = x and 3; voice.vibratoRetrig = (x and 4) == 0 }
|
||||||
0x4 -> { voice.tremoloWave = x and 3; voice.tremoloRetrig = (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 }
|
0x5 -> { voice.panbrelloWave = x and 3; voice.panbrelloRetrig = (x and 4) == 0 }
|
||||||
|
0x6 -> ts.finePatternDelayExtra += x // fine pattern delay: accumulate across channels
|
||||||
0x7 -> when (x) {
|
0x7 -> when (x) {
|
||||||
// Past-note actions on the channel's background ghosts.
|
// Past-note actions on the channel's background ghosts.
|
||||||
0x0 -> applyPastNoteAction(ts, vi, 0) // Past Note Cut
|
0x0 -> applyPastNoteAction(ts, vi, 0) // Past Note Cut
|
||||||
@@ -2142,6 +2158,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global volume slide (W coarse) — applied once per non-first tick per armed channel.
|
||||||
|
if (ts.tickInRow > 0) {
|
||||||
|
for (voice in ts.voices) {
|
||||||
|
if (voice.wSlideDir != 0) {
|
||||||
|
playhead.globalVolume = (playhead.globalVolume + voice.wSlideDir * voice.wSlideAmount).coerceIn(0, 0xFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Funk repeat (S$Fxxxx) — advance bit-mask per tick on instruments with active funkSpeed.
|
// Funk repeat (S$Fxxxx) — advance bit-mask per tick on instruments with active funkSpeed.
|
||||||
for (voice in ts.voices) {
|
for (voice in ts.voices) {
|
||||||
if (voice.funkSpeed == 0 || !voice.active) continue
|
if (voice.funkSpeed == 0 || !voice.active) continue
|
||||||
@@ -2241,7 +2266,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
ts.samplesIntoTick -= spt
|
ts.samplesIntoTick -= spt
|
||||||
applyTrackerTick(ts, playhead)
|
applyTrackerTick(ts, playhead)
|
||||||
ts.tickInRow++
|
ts.tickInRow++
|
||||||
if (ts.tickInRow >= playhead.tickRate) {
|
if (ts.tickInRow >= playhead.tickRate + ts.finePatternDelayExtra) {
|
||||||
ts.tickInRow = 0
|
ts.tickInRow = 0
|
||||||
advanceRow(ts, playhead)
|
advanceRow(ts, playhead)
|
||||||
}
|
}
|
||||||
@@ -2458,6 +2483,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var o: Int = 0
|
var o: Int = 0
|
||||||
var q: Int = 0
|
var q: Int = 0
|
||||||
var tslide: Int = 0
|
var tslide: Int = 0
|
||||||
|
var w: Int = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
class Voice {
|
class Voice {
|
||||||
@@ -2606,6 +2632,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var tempoSlideDir = 0 // 0 = none, -1 = down, +1 = up
|
var tempoSlideDir = 0 // 0 = none, -1 = down, +1 = up
|
||||||
var tempoSlideAmount = 0
|
var tempoSlideAmount = 0
|
||||||
|
|
||||||
|
// Global volume slide (W $xy00) — per-channel, applied to playhead.globalVolume on tick > 0.
|
||||||
|
var wSlideDir = 0 // 0 = none, -1 = down, +1 = up
|
||||||
|
var wSlideAmount = 0
|
||||||
|
|
||||||
// Volume / pan column slides (selectors 1/2/3 from TAUD_NOTE_EFFECTS.md §"Volume column effects").
|
// Volume / pan column slides (selectors 1/2/3 from TAUD_NOTE_EFFECTS.md §"Volume column effects").
|
||||||
var volColSlideUp = 0
|
var volColSlideUp = 0
|
||||||
var volColSlideDown = 0
|
var volColSlideDown = 0
|
||||||
@@ -2641,6 +2671,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// Channel index of the SEx that won this row (lowest channel wins ties).
|
// Channel index of the SEx that won this row (lowest channel wins ties).
|
||||||
var sexWinningChannel = -1
|
var sexWinningChannel = -1
|
||||||
|
|
||||||
|
// Fine pattern delay (S$6x) — extra ticks added to the current row; accumulated across all channels.
|
||||||
|
var finePatternDelayExtra = 0
|
||||||
|
|
||||||
// Pre-allocated mix buffers for dither path (reused each audio chunk).
|
// Pre-allocated mix buffers for dither path (reused each audio chunk).
|
||||||
val mixLeft = FloatArray(TRACKER_CHUNK)
|
val mixLeft = FloatArray(TRACKER_CHUNK)
|
||||||
val mixRight = FloatArray(TRACKER_CHUNK)
|
val mixRight = FloatArray(TRACKER_CHUNK)
|
||||||
@@ -2764,6 +2797,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
ts.pendingRowJumpLocal = false
|
ts.pendingRowJumpLocal = false
|
||||||
ts.patternDelayRemaining = 0; ts.patternDelayActive = false
|
ts.patternDelayRemaining = 0; ts.patternDelayActive = false
|
||||||
ts.sexWinningChannel = -1
|
ts.sexWinningChannel = -1
|
||||||
|
ts.finePatternDelayExtra = 0
|
||||||
ts.panLaw = initialGlobalFlags and 1
|
ts.panLaw = initialGlobalFlags and 1
|
||||||
ts.amigaMode = (initialGlobalFlags and 2) != 0
|
ts.amigaMode = (initialGlobalFlags and 2) != 0
|
||||||
ts.voices.forEach {
|
ts.voices.forEach {
|
||||||
|
|||||||
Reference in New Issue
Block a user