Compare commits

...

9 Commits

Author SHA1 Message Date
minjaesong
94e3ce55ce zfm: larger scroll peek window 2026-05-03 16:50:49 +09:00
minjaesong
9a9893b9a3 bios: using js version again 2026-05-03 16:50:35 +09:00
minjaesong
789c78f1e7 taud: more fixes 2026-05-03 16:50:24 +09:00
minjaesong
c7e7ee650d fix: no-param note handling divergence 2026-05-03 15:10:36 +09:00
minjaesong
5d968fecf5 fix: ImpulseTracker style instrument filters 2026-05-03 00:44:31 +09:00
minjaesong
aaf3cc28b2 Offset added to Taud instrument format doc 2026-05-03 00:30:57 +09:00
minjaesong
24375727db taud amiga period bug fix (multi-tick Exx/Fxx) 2026-05-02 23:52:58 +09:00
minjaesong
6a7ef670d9 fix: tracker mixer flags not setup properly on fresh boot 2026-05-02 23:43:07 +09:00
minjaesong
1bbf0de381 instrument volume fadeout 2026-05-02 21:13:00 +09:00
16 changed files with 396 additions and 200 deletions

9
.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
<option name="splitLayout" value="SHOW_EDITOR" />
</component>
</project>

View File

@@ -33,6 +33,10 @@ Current topics:
resonance damping curve, and the **IIR-only 2-pole topology** (NOT a resonance damping curve, and the **IIR-only 2-pole topology** (NOT a
biquad — no feedforward x[n1] / x[n2] terms) that `AudioAdapter.kt` uses biquad — no feedforward x[n1] / x[n2] terms) that `AudioAdapter.kt` uses
for Taud playback. for Taud playback.
- `reference_materials/ft2-clone` — Modernised clone for the original FastTracker 2
- `reference_materials/impulse-tracker` — The original source code for ImpulseTracker
- `reference_materials/MilkyTracker` — FastTracker 2 compatible tracker
- `reference_materials/schismtracker` — Open-source re-implementation of ImpulseTracker
When fetching new references, copy the relevant upstream files verbatim into When fetching new references, copy the relevant upstream files verbatim into
a topic folder, write a `README.md` summarising the relevant maths / a topic folder, write a `README.md` summarising the relevant maths /
@@ -88,12 +92,12 @@ Use the build scripts in `buildapp/`:
### Prerequisites ### Prerequisites
1. Download JDK 17 runtimes to `~/Documents/openjdk/*` with specific naming: 1. Download JDK 21 runtimes to `~/Documents/openjdk/*` with specific naming:
- `jdk-17.0.1-x86` (Linux AMD64) - `jdk-21.0.1-x86` (Linux AMD64)
- `jdk-17.0.1-arm` (Linux Aarch64) - `jdk-21.0.1-arm` (Linux Aarch64)
- `jdk-17.0.1-windows` (Windows AMD64) - `jdk-21.0.1-windows` (Windows AMD64)
- `jdk-17.0.1.jdk-arm` (macOS Apple Silicon) - `jdk-21.0.1.jdk-arm` (macOS Apple Silicon)
- `jdk-17.0.1.jdk-x86` (macOS Intel) - `jdk-21.0.1.jdk-x86` (macOS Intel)
2. Run `jlink` commands to create custom Java runtimes in `out/runtime-*` directories 2. Run `jlink` commands to create custom Java runtimes in `out/runtime-*` directories

View File

@@ -563,10 +563,26 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr
**Plain.** Applies Bitcrusher to the current voice. **Plain.** Applies Bitcrusher to the current voice.
- x: clipping mode. 0: clamp, 1: fold, 2: modulus - x: clipping mode. 0: clamp, 1: fold, 2: wrap
- y: bit depth (1..15). 8..15 has no effect on TSVM audio adapter (already operates on 8 bits) - y: bit depth (1..15). 8..15 has no effect on TSVM audio adapter (already operates on 8 bits)
- z: sample skip (0..255). 0: no skip, 1: use every 2nd samples, 2: use every 3rd samples, ..., 255: use every 256th samples - z: sample skip (0..255). 0: no skip, 1: use every 2nd samples, 2: use every 3rd samples, ..., 255: use every 256th samples
- `8 0000` will disable the bitcrusher - `8 0000` will disable the bitcrusher
- `8 x000` will modify the clipping mode shared effect symbol '9'
**Compatibility.** Unique to Taud. No compatible equivalent exists.
**Implementation.** TODO
---
## 9 $x0zz — Overdrive
**Plain.** Amplify the volume.
- x: clipping mode. 0: clamp, 1: fold, 2: wrap
- z: amplification. $00: 1x amplification (no extra volume), $01: 17/16 amplification, $02: 18/16 amplification, $10: 2x amplification (+ 6 dBFS), $F0: 16x amplification, $FF: 16.9375x amplification
- `9 0000` will reset the overdrive
- `9 x000` will modify the clipping mode shared with effect symbol '9'
**Compatibility.** Unique to Taud. No compatible equivalent exists. **Compatibility.** Unique to Taud. No compatible equivalent exists.
@@ -864,9 +880,9 @@ Effects in this section modifies the behaviour of the mixer. Primary intention o
## 1 $xx00 — Global behaviour flags ## 1 $xx00 — Global behaviour flags
**Plain.** Sets how the mixer should treat the panning. Available flags are: **Plain.** Sets mixer-wide behaviour flags. Available flags are:
0b 0000 00fp 0b 0000 0mfp
- p unset: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting. - p unset: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting.
- p set: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned. - p set: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned.
@@ -874,6 +890,9 @@ Effects in this section modifies the behaviour of the mixer. Primary intention o
- f unset: Linear tone mode. Pitch shift will behave like MIDI/ImpulseTracker/ScreamTracker linear mode. **Coarse and fine E/F arguments are stored as 4096-TET pitch units** and subtracted/added directly from the stored pitch. - f unset: Linear tone mode. Pitch shift will behave like MIDI/ImpulseTracker/ScreamTracker linear mode. **Coarse and fine E/F arguments are stored as 4096-TET pitch units** and subtracted/added directly from the stored pitch.
- f set: Amiga (cycle-based) tone mode. Pitch shift will behave like ProTracker/ScreamTracker default mode. **Coarse and fine E/F arguments are stored as raw tracker period units** (the unscaled byte/nibble from the source PT/S3M/IT file) and applied in Amiga period space. Tone portamento (G) remains linear regardless of mode. - f set: Amiga (cycle-based) tone mode. Pitch shift will behave like ProTracker/ScreamTracker default mode. **Coarse and fine E/F arguments are stored as raw tracker period units** (the unscaled byte/nibble from the source PT/S3M/IT file) and applied in Amiga period space. Tone portamento (G) remains linear regardless of mode.
- m unset: IT fadeout-zero policy. An instrument with stored volume fadeout = 0 does **not** fade out on key-off; the voice plays through until the volume envelope ends it (or never, if there is no envelope).
- m set: FT2 fadeout-zero policy. An instrument with stored volume fadeout = 0 is **cut** on the first tick after key-off (or NNA Note-Fade). Nonzero fadeouts behave identically in both modes — the per-tick decrement is always `fadeout / 65536` in unity-volume units.
**Implementation.** **Implementation.**
- Panning-linear: - Panning-linear:
- L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0 - L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0

Binary file not shown.

View File

@@ -1,5 +1,5 @@
con.reset_graphics();con.curs_set(0);con.clear(); con.reset_graphics();con.curs_set(0);con.clear();
graphics.resetPalette();graphics.setPalette(0, 0, 0, 0, 0);graphics.setBackground(0,0,0); graphics.resetPalette();graphics.setPalette(0, 0, 0, 0, 15);graphics.setBackground(0,0,0);
let logo = gzip.decomp(base64.atob("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")); let logo = gzip.decomp(base64.atob("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"));

View File

@@ -856,8 +856,8 @@ function drawControlHint() {
['sep'], ['sep'],
['Sp','Edit'], ['Sp','Edit'],
['sep'], ['sep'],
['n','Solo'],
['m','Mute'], ['m','Mute'],
['s','Solo'],
['sep'], ['sep'],
['Tab','Panel'] ['Tab','Panel']
// ['sep'], // ['sep'],
@@ -1397,7 +1397,7 @@ function timelineInput(wo, event) {
drawVoiceHeaders(); drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail() drawVoiceHeaders(); drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail()
} }
else if (keyJustHit && !shiftDown && event.includes(keys.M)) { toggleMute(cursorVox) } else if (keyJustHit && !shiftDown && event.includes(keys.M)) { toggleMute(cursorVox) }
else if (keyJustHit && !shiftDown && event.includes(keys.S)) { toggleSolo(cursorVox) } else if (keyJustHit && !shiftDown && event.includes(keys.N)) { toggleSolo(cursorVox) }
return return
} }
@@ -1434,7 +1434,7 @@ function timelineInput(wo, event) {
} }
if (keyJustHit && !shiftDown && event.includes(keys.M)) { toggleMute(cursorVox); return } if (keyJustHit && !shiftDown && event.includes(keys.M)) { toggleMute(cursorVox); return }
if (keyJustHit && !shiftDown && event.includes(keys.S)) { toggleSolo(cursorVox); return } if (keyJustHit && !shiftDown && event.includes(keys.N)) { toggleSolo(cursorVox); return }
if (keysym === "<UP>") { cursorRow -= moveDelta; rowMove = true } if (keysym === "<UP>") { cursorRow -= moveDelta; rowMove = true }
else if (keysym === "<DOWN>") { cursorRow += moveDelta; rowMove = true } else if (keysym === "<DOWN>") { cursorRow += moveDelta; rowMove = true }
@@ -1933,6 +1933,8 @@ function drawProjectContents(wo) {
Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`, Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`,
Notation: pitchTablePresets[PITCH_PRESET_IDX].name, Notation: pitchTablePresets[PITCH_PRESET_IDX].name,
Flags: `${flagstrbuf} ($${mixerflag.hex02()})`, Flags: `${flagstrbuf} ($${mixerflag.hex02()})`,
GlobalVol: initialGlobalVolume,
MixingVol: initialMixingVolume
} }
Object.entries(projMeta).forEach(([key, value], index) => { Object.entries(projMeta).forEach(([key, value], index) => {

View File

@@ -422,6 +422,8 @@ let filenavOninput = (window, event) => {
let keycodes = [event[3],event[4],event[5],event[6],event[7],event[8],event[9],event[10]] let keycodes = [event[3],event[4],event[5],event[6],event[7],event[8],event[9],event[10]]
let keycode = keycodes[0] let keycode = keycodes[0]
let scrollPeek = (LIST_HEIGHT / 3)|0
if (keyJustHit && keysym == "q") { if (keyJustHit && keysym == "q") {
exit = true exit = true
} }
@@ -430,19 +432,19 @@ let filenavOninput = (window, event) => {
redraw() // this would double-redraw (hence no panel switching) or something if redraw() is not merely a request to do so redraw() // this would double-redraw (hence no panel switching) or something if redraw() is not merely a request to do so
} }
else if (keysym == "<UP>") { else if (keysym == "<UP>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1) [cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel() drawFilePanel()
} }
else if (keysym == "<DOWN>") { else if (keysym == "<DOWN>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1) [cursor[windowMode], scroll[windowMode]] = win.scrollVert(+1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel() drawFilePanel()
} }
else if (keysym == "<PAGE_UP>") { else if (keysym == "<PAGE_UP>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1) [cursor[windowMode], scroll[windowMode]] = win.scrollVert(-LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel() drawFilePanel()
} }
else if (keysym == "<PAGE_DOWN>") { else if (keysym == "<PAGE_DOWN>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1) [cursor[windowMode], scroll[windowMode]] = win.scrollVert(+LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel() drawFilePanel()
} }
else if (keyJustHit && keycode == 66) { // enter else if (keyJustHit && keycode == 66) { // enter

View File

@@ -96,8 +96,6 @@ class WindowObject {
* @return [new cursor pos, new scroll pos] * @return [new cursor pos, new scroll pos]
*/ */
function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos, scrollPeek) { function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos, scrollPeek) {
let peek = 1
// clamp dy // clamp dy
if (currentCursorPos + dy > listSize - 1) if (currentCursorPos + dy > listSize - 1)
dy = (listSize - 1) - currentCursorPos dy = (listSize - 1) - currentCursorPos
@@ -108,13 +106,13 @@ function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos
// update vertical scroll stats // update vertical scroll stats
if (dy != 0) { if (dy != 0) {
let visible = listHeight - 1 - peek let visible = listHeight - 1 - scrollPeek
if (nextRow - currentScrollPos > visible) { if (nextRow - currentScrollPos > visible) {
currentScrollPos = nextRow - visible currentScrollPos = nextRow - visible
} }
else if (nextRow - currentScrollPos < 0 + peek) { else if (nextRow - currentScrollPos < 0 + scrollPeek) {
currentScrollPos = nextRow - peek // nextRow is less than zero currentScrollPos = nextRow - scrollPeek // nextRow is less than zero
} }
// NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down // NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down
@@ -145,8 +143,6 @@ function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos
* @return [new cursor pos, new scroll pos] * @return [new cursor pos, new scroll pos]
*/ */
function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScrollPos, scrollPeek) { function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScrollPos, scrollPeek) {
let peek = 1
// clamp dx // clamp dx
if (currentCursorPos + dx > stringSize - 1) if (currentCursorPos + dx > stringSize - 1)
dx = (stringSize - 1) - currentCursorPos dx = (stringSize - 1) - currentCursorPos
@@ -157,13 +153,13 @@ function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScr
// update vertical scroll stats // update vertical scroll stats
if (dx != 0) { if (dx != 0) {
let visible = stringViewSize - 1 - peek let visible = stringViewSize - 1 - scrollPeek
if (nextCol - currentScrollPos > visible) { if (nextCol - currentScrollPos > visible) {
currentScrollPos = nextCol - visible currentScrollPos = nextCol - visible
} }
else if (nextCol - currentScrollPos < 0 + peek) { else if (nextCol - currentScrollPos < 0 + scrollPeek) {
currentScrollPos = nextCol - peek // nextCol is less than zero currentScrollPos = nextCol - scrollPeek // nextCol is less than zero
} }
// NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down // NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down

View File

@@ -300,9 +300,13 @@ def _it214_decompress_block(payload: bytes, num_samples: int,
delta = _sign_extend(v, width) delta = _sign_extend(v, width)
is_data = True is_data = True
elif width < init_width: elif width < init_width:
# Mode B (mid): `range_count` escape codes centred on signed midpoint. # Mode B (mid): `range_count` escape codes occupy values (border, border+range_count].
# The encoder simply does NOT emit data values that would collide with this slot —
# it widens first. So values *above* the escape range are sign-extended verbatim,
# not collapsed. Reference: schismtracker fmt/compression.c:103-127 and
# MilkyTracker XModule.cpp:629-640.
# border = (mask >> (init_width-width)) - border_sub, where border_sub # border = (mask >> (init_width-width)) - border_sub, where border_sub
# = range_count / 2. Reference: libxmp it_compress.c, OpenMPT ITTools.cpp. # = range_count / 2.
# 8-bit: width=7 → border=63-4=59, width=8 → border=127-4=123 # 8-bit: width=7 → border=63-4=59, width=8 → border=127-4=123
# 16-bit: width=7..16 with border_sub=8. # 16-bit: width=7..16 with border_sub=8.
border = (mask >> (init_width - width)) - border_sub border = (mask >> (init_width - width)) - border_sub
@@ -310,8 +314,6 @@ def _it214_decompress_block(payload: bytes, num_samples: int,
new_w = v - border new_w = v - border
width = new_w if new_w < width else new_w + 1 # skip-self width = new_w if new_w < width else new_w + 1 # skip-self
continue continue
if v > border + range_count:
v -= range_count # collapse escape range out
delta = _sign_extend(v, width) delta = _sign_extend(v, width)
is_data = True is_data = True
else: else:
@@ -467,12 +469,13 @@ class ITInstrument:
__slots__ = ('name', 'dfp', 'gv', 'canonical_sample', 'canonical_volume', __slots__ = ('name', 'dfp', 'gv', 'canonical_sample', 'canonical_volume',
'vol_envelope', 'pan_envelope', 'vol_env_sustain', 'pan_env_sustain', 'vol_envelope', 'pan_envelope', 'vol_env_sustain', 'pan_env_sustain',
'pf_envelope', 'pf_env_sustain', 'pf_is_filter', 'pf_envelope', 'pf_env_sustain', 'pf_is_filter',
'ifc', 'ifr', 'fadeout', 'pps', 'ppc', 'rv', 'rp', 'nna') 'ifc', 'ifr', 'fadeout', 'pps', 'ppc', 'rv', 'rp', 'nna',
'dct', 'dca')
# vol_envelope / pan_envelope / pf_envelope: list of 25 (value, minifloat_idx) tuples, or None # vol_envelope / pan_envelope / pf_envelope: list of 25 (value, minifloat_idx) tuples, or None
# *_env_sustain: int (16-bit, 0b 0ut sssss pcb eeeee), 0 = no envelope # *_env_sustain: int (16-bit, 0b 0ut sssss pcb eeeee), 0 = no envelope
# pf_is_filter: bool — pf envelope mode (False = pitch, True = filter) # pf_is_filter: bool — pf envelope mode (False = pitch, True = filter)
# ifc / ifr : initial filter cutoff / resonance (0..127, 0 if not set) # ifc / ifr : initial filter cutoff / resonance (0..127, 0 if not set)
# fadeout : 0..1024 (IT FadeOut field, applied per tick after key-off) # fadeout : 0..1024 (IT FadeOut field; doubled to 0..2048 when written to Taud's 12-bit field)
# pps / ppc : pitch-pan separation (signed -32..+32) and centre note (0..119) # pps / ppc : pitch-pan separation (signed -32..+32) and centre note (0..119)
# rv / rp : random volume swing (0..100) / random pan swing (0..64) # rv / rp : random volume swing (0..100) / random pan swing (0..64)
# nna : new note action (IT 0=cut, 1=continue, 2=note off, 3=note fade) # nna : new note action (IT 0=cut, 1=continue, 2=note off, 3=note fade)
@@ -489,6 +492,11 @@ def parse_instruments(data: bytes, h: ITHeader) -> list:
inst.name = data[ptr+0x20:ptr+0x3A].rstrip(b'\x00').decode('latin-1', errors='replace') inst.name = data[ptr+0x20:ptr+0x3A].rstrip(b'\x00').decode('latin-1', errors='replace')
# NNA at IMPI+0x11 (new format). 0=cut, 1=continue, 2=note off, 3=note fade. # NNA at IMPI+0x11 (new format). 0=cut, 1=continue, 2=note off, 3=note fade.
inst.nna = data[ptr + 0x11] & 0x03 inst.nna = data[ptr + 0x11] & 0x03
# DCT (Duplicate Check Type) and DCA (Duplicate Check Action), per Schism iti.c:80-94.
# DCT: 0=off, 1=note, 2=sample, 3=instrument.
# DCA: 0=note cut, 1=note off, 2=note fade.
inst.dct = data[ptr + 0x12] & 0x03
inst.dca = data[ptr + 0x13] & 0x03
inst.fadeout = struct.unpack_from('<H', data, ptr + 0x14)[0] # 0..1024 inst.fadeout = struct.unpack_from('<H', data, ptr + 0x14)[0] # 0..1024
# PPS is signed -32..+32; PPC is the centre note (IT note number 0..119, C-5=60). # PPS is signed -32..+32; PPC is the centre note (IT note number 0..119, C-5=60).
inst.pps = struct.unpack_from('b', data, ptr + 0x16)[0] inst.pps = struct.unpack_from('b', data, ptr + 0x16)[0]
@@ -516,9 +524,12 @@ def parse_instruments(data: bytes, h: ITHeader) -> list:
inst.canonical_sample = c5_smp # 1-based sample index, 0 = none inst.canonical_sample = c5_smp # 1-based sample index, 0 = none
inst.canonical_volume = min(inst.gv, 64) inst.canonical_volume = min(inst.gv, 64)
# Initial filter cutoff/resonance (high bit = enabled, low 7 bits = value) # Initial filter cutoff/resonance (high bit = enabled, low 7 bits = value).
ifc_raw = data[ptr + 0x39] # Per Schism iti.c struct it_instrument: name[26] occupies 0x20..0x39,
ifr_raw = data[ptr + 0x3A] # ifc is at 0x3A, ifr at 0x3B. Off-by-one would silently disable filters
# on every IT instrument because name's last byte is always NUL.
ifc_raw = data[ptr + 0x3A]
ifr_raw = data[ptr + 0x3B]
# Taud uses full 0..255 range (double IT's resolution): IT 0..127 → Taud 0..254, # Taud uses full 0..255 range (double IT's resolution): IT 0..127 → Taud 0..254,
# IT "off" (high bit clear) → Taud 255. # IT "off" (high bit clear) → Taud 255.
inst.ifc = (ifc_raw & 0x7F) * 2 if (ifc_raw & 0x80) else 255 inst.ifc = (ifc_raw & 0x7F) * 2 if (ifc_raw & 0x80) else 255
@@ -1114,7 +1125,7 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
vol_env, vol_sus, pan_env, pan_sus, pf_env, pf_sus, pf_is_filter, vol_env, vol_sus, pan_env, pan_sus, pf_env, pf_sus, pf_is_filter,
inst_gv, fadeout, vib_speed, vib_depth, vib_sweep, vib_rate, vib_wave, inst_gv, fadeout, vib_speed, vib_depth, vib_sweep, vib_rate, vib_wave,
default_pan, pps, ppc_taud, pan_swing, vol_swing, ifc, ifr, default_pan, pps, ppc_taud, pan_swing, vol_swing, ifc, ifr,
sample_detune, nna. sample_detune, nna, dct, dca.
All optional; missing keys default to neutral values. All optional; missing keys default to neutral values.
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict). Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict).
@@ -1209,7 +1220,7 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
struct.pack_into('<I', inst_bin, base + 0, ptr) struct.pack_into('<I', inst_bin, base + 0, ptr)
struct.pack_into('<H', inst_bin, base + 4, s_len) struct.pack_into('<H', inst_bin, base + 4, s_len)
struct.pack_into('<H', inst_bin, base + 6, c2spd) struct.pack_into('<H', inst_bin, base + 6, c2spd)
struct.pack_into('<H', inst_bin, base + 8, 0) # play start struct.pack_into('<H', inst_bin, base + 8, 0) # play start. IT samples always start playing from zero
struct.pack_into('<H', inst_bin, base + 10, ls) struct.pack_into('<H', inst_bin, base + 10, ls)
struct.pack_into('<H', inst_bin, base + 12, le) struct.pack_into('<H', inst_bin, base + 12, le)
inst_bin[base + 14] = flags_byte inst_bin[base + 14] = flags_byte
@@ -1221,8 +1232,20 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
vol_sus = idata.get('vol_sus', USE_ENV_BIT) vol_sus = idata.get('vol_sus', USE_ENV_BIT)
pan_sus = idata.get('pan_sus', 0) pan_sus = idata.get('pan_sus', 0)
pf_sus = idata.get('pf_sus', 0) pf_sus = idata.get('pf_sus', 0)
inst_gv = idata.get('inst_gv', 0xFF) # Sample-mode default IGV: fold sample default vol (Sv) and sample GV
fadeout = idata.get('fadeout', 0) & 0x3FF # 10-bit (low 8 + high 2) # into Taud's IGV. Instrument-mode supplies inst_gv pre-folded.
if 'inst_gv' in idata:
inst_gv = idata['inst_gv']
else:
smp_vol_default = min(getattr(s, 'vol', 64), 64)
smp_gv_default = min(getattr(s, 'gv', 64), 64)
inst_gv = min(255, round(smp_vol_default * smp_gv_default * 255 / (64 * 64)))
# IT fadeout (file-stored 0..2048; ITTECH practical max ≈ 1024) maps verbatim to
# the Taud 12-bit fadeStep. The player picks divisor 1024 in IT mode (vs 65536
# in FT2 mode) so that one fadeStep unit per tick matches Schism's
# `chan->fadeout_volume -= (stored<<5)<<1` semantics (sndmix.c:331-339,
# effects.c:1261). Clamp defensively to 4095.
fadeout = min(0xFFF, idata.get('fadeout', 0) & 0xFFFF)
struct.pack_into('<H', inst_bin, base + 15, vol_sus & 0xFFFF) struct.pack_into('<H', inst_bin, base + 15, vol_sus & 0xFFFF)
struct.pack_into('<H', inst_bin, base + 17, pan_sus & 0xFFFF) struct.pack_into('<H', inst_bin, base + 17, pan_sus & 0xFFFF)
@@ -1231,8 +1254,9 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
if vol_env: if vol_env:
_write_env(inst_bin, base + 21, vol_env) _write_env(inst_bin, base + 21, vol_env)
else: else:
# Single-point: hold at sample default volume. # Single-point envelope held at full-scale; the per-sample level is
inst_bin[base + 21] = min(getattr(s, 'vol', 63), 63) # carried by IGV (byte 171), so the envelope must be a unit multiplier.
inst_bin[base + 21] = 63
inst_bin[base + 22] = 0 inst_bin[base + 22] = 0
# Force engine to use this single point. # Force engine to use this single point.
struct.pack_into('<H', inst_bin, base + 15, USE_ENV_BIT) struct.pack_into('<H', inst_bin, base + 15, USE_ENV_BIT)
@@ -1280,7 +1304,13 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
inst_bin[base + 187] = idata.get('vib_depth', 0) & 0xFF inst_bin[base + 187] = idata.get('vib_depth', 0) & 0xFF
# Byte 188: vibrato rate (0..255 full range, IT samplewise Vir). # Byte 188: vibrato rate (0..255 full range, IT samplewise Vir).
inst_bin[base + 188] = idata.get('vib_rate', 0) & 0xFF inst_bin[base + 188] = idata.get('vib_rate', 0) & 0xFF
# Bytes 189-191: reserved (already zeroed). # Byte 189: duplicate-check / action (IT-only — bits 0-1 = DCT, bits 2-3 = DCA).
# DCT: 0=off, 1=note, 2=sample, 3=instrument.
# DCA: 0=note cut, 1=note off, 2=note fade.
dct = idata.get('dct', 0) & 0x03
dca = idata.get('dca', 0) & 0x03
inst_bin[base + 189] = (dca << 2) | dct
# Bytes 190-191: reserved (already zeroed).
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}") vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
@@ -1303,7 +1333,6 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
rows = chunk_grid[ch_idx] if ch_idx < len(chunk_grid) else [ITRow()] * PATTERN_ROWS rows = chunk_grid[ch_idx] if ch_idx < len(chunk_grid) else [ITRow()] * PATTERN_ROWS
last_inst = 0 last_inst = 0
last_note_it = -1 last_note_it = -1
last_vol = None
for r, cell in enumerate(rows[:PATTERN_ROWS]): for r, cell in enumerate(rows[:PATTERN_ROWS]):
# ── Resolve vol-col into overrides ────────────────────────────────── # ── Resolve vol-col into overrides ──────────────────────────────────
@@ -1337,30 +1366,21 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
note_triggers = (0 <= (cell.note if cell.note >= 0 else -1) <= 119) note_triggers = (0 <= (cell.note if cell.note >= 0 else -1) <= 119)
# ── Volume column ──────────────────────────────────────────────────── # ── Volume column ────────────────────────────────────────────────────
# Priority: explicit cell vol (from vol-col 0-64) > note-trigger default # Priority: explicit cell vol (vol-col 0-64) > vol-col slide > main-
# > retrigger recall > vol-col slide > main-effect vol override > nop # effect vol override > nop. The per-instrument default volume is
# baked into IGV (byte 171), so the engine resolves note-trigger
# default volume itself; the converter no longer emits SEL_SET=Sv.
if cell.volcol >= 0 and cell.volcol <= VC_VOL_HI: if cell.volcol >= 0 and cell.volcol <= VC_VOL_HI:
vol_sel, vol_value = SEL_SET, min(cell.volcol, 0x3F) vol_sel, vol_value = SEL_SET, min(cell.volcol, 0x3F)
elif note_triggers and cell.inst > 0:
vol_sel = SEL_SET
vol_value = inst_vols.get(last_inst, 0x3F)
elif note_triggers and last_vol is not None:
vol_sel, vol_value = SEL_SET, last_vol
elif (cell.inst > 0 and cell.note < 0
and last_note_it >= 0 and last_vol is not None):
# Instrument-only retrigger: restate last volume
vol_sel, vol_value = SEL_SET, last_vol
elif vol_override is not None:
vol_sel, vol_value = vol_override
elif vs != SEL_FINE or vv != 0: elif vs != SEL_FINE or vv != 0:
vol_sel, vol_value = vs, vv vol_sel, vol_value = vs, vv
elif vol_override is not None:
vol_sel, vol_value = vol_override
else: else:
vol_sel, vol_value = SEL_FINE, 0 vol_sel, vol_value = SEL_FINE, 0
if cell.note is not None and 0 <= (cell.note if cell.note >= 0 else -1) <= 119: if cell.note is not None and 0 <= (cell.note if cell.note >= 0 else -1) <= 119:
last_note_it = cell.note last_note_it = cell.note
if vol_sel == SEL_SET:
last_vol = vol_value
# ── Pan column ─────────────────────────────────────────────────────── # ── Pan column ───────────────────────────────────────────────────────
if cell.pan_set is not None: if cell.pan_set is not None:
@@ -1592,15 +1612,18 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
src_smp = samples[si] src_smp = samples[si]
proxy[taud_slot] = src_smp proxy[taud_slot] = src_smp
# IT cell-trigger initial volume comes from the sample's default # IT cell-trigger initial volume comes from the sample's default
# volume (Dv, 0..64), not the instrument's global volume. # volume (Sv, 0..64). It is folded into the Taud instrument's IGV
# (byte 171) along with IT inst.gv (0..128) and sample gv (0..64),
# so the engine applies all three as a single multiplier on every
# fresh trigger. inst_vols is retained only for legacy callers.
smp_default_vol = min(getattr(src_smp, 'vol', 64), 64) smp_default_vol = min(getattr(src_smp, 'vol', 64), 64)
inst_vols[taud_slot] = min(smp_default_vol, 0x3F) inst_vols[taud_slot] = min(smp_default_vol, 0x3F)
# IT instrument GV (0..128) and sample GV (0..64) collapse into # IT inst.gv (0..128) * sample.gv (0..64) * sample.vol (0..64)
# Taud's single instrumentwise GV (0..255). Sample default volume # collapse into Taud's single instrumentwise IGV (0..255).
# is handled separately by inst_vols above.
smp_gv = min(getattr(src_smp, 'gv', 64), 64) smp_gv = min(getattr(src_smp, 'gv', 64), 64)
inst_gv_255 = min(255, round(inst.gv * smp_gv * 255 / (128 * 64))) inst_gv_255 = min(255, round(inst.gv * smp_gv * smp_default_vol * 255
/ (128 * 64 * 64)))
# IT pitch-pan centre: note number 0..119 (C-5 = 60). The Taud # IT pitch-pan centre: note number 0..119 (C-5 = 60). The Taud
# representation is the absolute 4096-TET note value used in patterns # representation is the absolute 4096-TET note value used in patterns
@@ -1620,14 +1643,14 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
default_pan = 0x80 default_pan = 0x80
# Auto-vibrato lives on the canonical sample (not the IT instrument). # Auto-vibrato lives on the canonical sample (not the IT instrument).
# IT samplewise auto-vibrato: Vis (speed 0..64), Vid (depth 0..32), # IT samplewise auto-vibrato: Vis (speed 0..64), Vid (depth 0..64),
# Vir (rate 0..255 — IT-style ramp-in), Vit (waveform 0..3). # Vir (rate 0..255 — IT-style ramp-in), Vit (waveform 0..3).
# Taud byte 175 (Vibrato Speed) follows FT2 0..255 scale: rescale Vis. # Taud byte 175 (Vibrato Speed) follows FT2 0..255 scale: rescale Vis.
# Taud byte 187 (Vibrato Depth) is full 0..255: rescale Vid 0..32 → 0..255. # Taud byte 187 (Vibrato Depth) is full 0..255: rescale Vid 0..64 → 0..255.
# Taud byte 188 (Vibrato Rate) is IT Vir verbatim. # Taud byte 188 (Vibrato Rate) is IT Vir verbatim.
# Taud byte 176 (Vibrato Sweep) is FT2-only — leave 0 for IT. # Taud byte 176 (Vibrato Sweep) is FT2-only — leave 0 for IT.
vib_speed_taud = min(255, round(src_smp.av_speed * 255 / 64)) vib_speed_taud = min(255, round(src_smp.av_speed * 255 / 64))
vib_depth_taud = min(255, round(src_smp.av_depth * 255 / 32)) vib_depth_taud = min(255, round(src_smp.av_depth * 255 / 64))
# IT NNA (0=cut, 1=continue, 2=note off, 3=note fade) → # IT NNA (0=cut, 1=continue, 2=note off, 3=note fade) →
# Taud NNA (00=note off, 01=cut, 10=continue, 11=fade). # Taud NNA (00=note off, 01=cut, 10=continue, 11=fade).
it_to_taud_nna = (0b01, 0b10, 0b00, 0b11) it_to_taud_nna = (0b01, 0b10, 0b00, 0b11)
@@ -1656,6 +1679,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
'ifr': inst.ifr, 'ifr': inst.ifr,
'sample_detune': 0, # IT samples have no finetune 'sample_detune': 0, # IT samples have no finetune
'nna': nna_taud, 'nna': nna_taud,
'dct': inst.dct,
'dca': inst.dca,
} }
sampleinst_raw, _ = build_sample_inst_bin_it(proxy, instr_data_by_slot) sampleinst_raw, _ = build_sample_inst_bin_it(proxy, instr_data_by_slot)
else: else:
@@ -1741,6 +1766,7 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
vprint(f" cue sheet: {len(sheet)}{len(cue_comp)} bytes (gzip)") vprint(f" cue sheet: {len(sheet)}{len(cue_comp)} bytes (gzip)")
# flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted) # flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted)
# bit 2 (m) cleared: IT fadeout-zero policy — stored fadeout=0 means "no fadeout".
flags_byte = 0x00 if h.linear_slides else 0x02 flags_byte = 0x00 if h.linear_slides else 0x02
# IT global/mix volumes are 0..128; rescale to Taud's 0..255 (clamped). # IT global/mix volumes are 0..128; rescale to Taud's 0..255 (clamped).
global_vol_taud = min(0xFF, round(h.global_vol * 255 / 128)) global_vol_taud = min(0xFF, round(h.global_vol * 255 / 128))

View File

@@ -506,7 +506,9 @@ def build_sample_inst_bin(samples: list) -> tuple:
le = min(s.loop_end, 65535) le = min(s.loop_end, 65535)
loop_mode = 1 if (s.flags & 1) else 0 loop_mode = 1 if (s.flags & 1) else 0
flags_byte = loop_mode & 0x3 flags_byte = loop_mode & 0x3
env_vol = min(s.volume, 63) # Envelope first point is full-scale; per-sample level is carried by
# IGV (byte 171) so the envelope must contribute a unit multiplier.
env_vol = 63
vol_env_flags = 0x0020 # use-envelope bit vol_env_flags = 0x0020 # use-envelope bit
base = taud_idx * 192 base = taud_idx * 192
@@ -522,7 +524,10 @@ def build_sample_inst_bin(samples: list) -> tuple:
struct.pack_into('<H', inst_bin, base + 19, 0) struct.pack_into('<H', inst_bin, base + 19, 0)
inst_bin[base + 21] = env_vol inst_bin[base + 21] = env_vol
inst_bin[base + 22] = 0 inst_bin[base + 22] = 0
inst_bin[base + 171] = 0xFF # instrument global volume # Instrument Global Volume carries the MOD sample's default volume (0..64 → 0..255).
# The pattern builder no longer emits SEL_SET=Sv on note triggers; the engine
# multiplies by IGV instead, so the per-instrument level lives here.
inst_bin[base + 171] = min(0xFF, round(min(s.volume, 64) * 255 / 64))
inst_bin[base + 177] = 0x80 # default pan = centre (unused; pan env "p" flag not set) inst_bin[base + 177] = 0x80 # default pan = centre (unused; pan env "p" flag not set)
inst_bin[base + 182] = 0xFF # filter cutoff = off inst_bin[base + 182] = 0xFF # filter cutoff = off
inst_bin[base + 183] = 0xFF # filter resonance = off inst_bin[base + 183] = 0xFF # filter resonance = off
@@ -546,15 +551,15 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
inst_vols: dict) -> bytes: inst_vols: dict) -> bytes:
"""Build a 512-byte Taud pattern for one MOD channel. """Build a 512-byte Taud pattern for one MOD channel.
Volume column rules (mirrors s3m2taud): Volume column: explicit Cxx → SEL_SET; effect-folded vol slide → vol_override;
explicit Cxx vol > note-trigger inst default > instrument-only retrigger otherwise SEL_FINE/0 (no-op). Per-instrument default volume lives in IGV
recall > vol_override from effect > no-op. (byte 171) and is applied by the engine on every fresh trigger — the
converter no longer has to emit SEL_SET=Sv to scale notes.
""" """
out = bytearray(PATTERN_BYTES) out = bytearray(PATTERN_BYTES)
rows = grid[ch_idx] if ch_idx < len(grid) else [ModRow()] * MOD_PATTERN_ROWS rows = grid[ch_idx] if ch_idx < len(grid) else [ModRow()] * MOD_PATTERN_ROWS
last_inst = 0 last_inst = 0
last_period = 0 last_period = 0
last_vol = None
for r, row in enumerate(rows[:MOD_PATTERN_ROWS]): for r, row in enumerate(rows[:MOD_PATTERN_ROWS]):
note_taud = period_to_taud_note(row.period) note_taud = period_to_taud_note(row.period)
note_triggers = (row.period > 0) note_triggers = (row.period > 0)
@@ -562,10 +567,6 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
if row.inst > 0: if row.inst > 0:
last_inst = row.inst last_inst = row.inst
retrigger = (row.inst > 0
and row.period == 0
and last_period > 0)
op, arg, vol_override, pan_override = encode_effect( op, arg, vol_override, pan_override = encode_effect(
row.effect, row.effect_arg, ch_idx, r) row.effect, row.effect_arg, ch_idx, r)
@@ -575,13 +576,6 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
if vol_override is not None and vol_override[0] != SEL_SET: if vol_override is not None and vol_override[0] != SEL_SET:
vprint(f" ch{ch_idx} row{r}: dropped vol slide " vprint(f" ch{ch_idx} row{r}: dropped vol slide "
f"(cell already carries explicit Cxx volume)") f"(cell already carries explicit Cxx volume)")
elif note_triggers and row.inst > 0:
vol_sel = SEL_SET
vol_value = inst_vols.get(last_inst, 0x3F)
elif note_triggers and last_vol is not None:
vol_sel, vol_value = SEL_SET, last_vol
elif retrigger and last_vol is not None:
vol_sel, vol_value = SEL_SET, last_vol
elif vol_override is not None: elif vol_override is not None:
vol_sel, vol_value = vol_override vol_sel, vol_value = vol_override
else: else:
@@ -589,8 +583,6 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
if note_triggers: if note_triggers:
last_period = row.period last_period = row.period
if vol_sel == SEL_SET:
last_vol = vol_value
# ── Pan column ── # ── Pan column ──
if pan_override is not None: if pan_override is not None:
@@ -758,7 +750,9 @@ def assemble_taud(mod: dict) -> bytes:
# ProTracker is Amiga-period-based by definition, so we set the f bit so # ProTracker is Amiga-period-based by definition, so we set the f bit so
# the engine applies coarse pitch slides in period space (recovers PT's # the engine applies coarse pitch slides in period space (recovers PT's
# characteristic non-linear pitch character). # characteristic non-linear pitch character).
flags_byte = 0x02 # bit 2 (m) set: FT2 fadeout-zero policy — PT has no fadeout, so the stored
# zero on every instrument means "cut on key-off" (unified with S3M imports).
flags_byte = 0x02 | 0x04
song_table = encode_song_entry( song_table = encode_song_entry(
song_offset=song_offset, song_offset=song_offset,
num_voices=n_channels, num_voices=n_channels,
@@ -771,7 +765,7 @@ def assemble_taud(mod: dict) -> bytes:
pat_bin_comp_size=len(pat_comp), pat_bin_comp_size=len(pat_comp),
cue_sheet_comp_size=len(cue_comp), cue_sheet_comp_size=len(cue_comp),
global_vol=0xFF, global_vol=0xFF,
mixing_vol=0xFF, mixing_vol=180,
) )
assert len(song_table) == TAUD_SONG_ENTRY assert len(song_table) == TAUD_SONG_ENTRY

View File

@@ -495,8 +495,9 @@ def build_sample_inst_bin(instruments: list) -> tuple:
loop_mode = 1 if (inst.flags & 1) else 0 loop_mode = 1 if (inst.flags & 1) else 0
flags_byte = loop_mode & 0x3 # 0b 0000 00pp flags_byte = loop_mode & 0x3 # 0b 0000 00pp
# Volume envelope: hold at instrument volume (clamped to 0x3F). # Volume envelope first point is full-scale; per-sample level is carried
env_vol = min(inst.volume, 63) # by IGV (byte 171) so the envelope contributes a unit multiplier.
env_vol = 63
# Vol env-flags: enable use-envelope bit (b=1) so engine reads the single point. # Vol env-flags: enable use-envelope bit (b=1) so engine reads the single point.
vol_env_flags = 0x0020 # b=bit 5 vol_env_flags = 0x0020 # b=bit 5
@@ -514,7 +515,10 @@ def build_sample_inst_bin(instruments: list) -> tuple:
# Volume env point 0: hold at env_vol indefinitely (offset minifloat = 0 → hold). # Volume env point 0: hold at env_vol indefinitely (offset minifloat = 0 → hold).
inst_bin[base + 21] = env_vol inst_bin[base + 21] = env_vol
inst_bin[base + 22] = 0 inst_bin[base + 22] = 0
inst_bin[base + 171] = 0xFF # instrument global volume # Instrument Global Volume carries the S3M instrument's default volume (0..64 → 0..255).
# The pattern builder no longer emits SEL_SET=Sv on note triggers; the engine
# multiplies by IGV instead, so the per-instrument level lives here.
inst_bin[base + 171] = min(0xFF, round(min(inst.volume, 64) * 255 / 64))
inst_bin[base + 177] = 0x80 # default pan = centre (unused; pan env "p" flag not set) inst_bin[base + 177] = 0x80 # default pan = centre (unused; pan env "p" flag not set)
inst_bin[base + 182] = 0xFF # filter cutoff = off inst_bin[base + 182] = 0xFF # filter cutoff = off
inst_bin[base + 183] = 0xFF # filter resonance = off inst_bin[base + 183] = 0xFF # filter resonance = off
@@ -544,11 +548,10 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
amiga_mode: bool = False) -> bytes: amiga_mode: bool = False) -> bytes:
"""Build a 512-byte Taud pattern for one S3M channel. """Build a 512-byte Taud pattern for one S3M channel.
Volume column: explicit S3M cell vol SEL_SET; when a note triggers Volume column: explicit S3M cell vol -> SEL_SET; M/N/K/L vol slides folded
with no explicit vol, emit SEL_SET using the instrument's default volume by encode_effect -> vol_override; otherwise SEL_FINE/0 (no-op). Per-
(looked up from inst_vols, a 1-based inst index → 0..63 volume dict). instrument default volume lives in IGV (byte 171) and is applied by the
M/N/K/L overrides apply only when the cell has no explicit vol and no engine on every fresh trigger, so the converter no longer emits SEL_SET=Sv.
note trigger. Otherwise SEL_FINE/0 (no-op).
Pan column: row 0 emits SEL_SET = default_pan to position the channel; Pan column: row 0 emits SEL_SET = default_pan to position the channel;
other rows default to SEL_FINE/0 unless an X/P/etc effect overrides. other rows default to SEL_FINE/0 unless an X/P/etc effect overrides.
""" """
@@ -557,55 +560,27 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
out = bytearray(PATTERN_BYTES) out = bytearray(PATTERN_BYTES)
rows = s3m_grid[ch_idx] if ch_idx < len(s3m_grid) else [S3MRow()] * PATTERN_ROWS rows = s3m_grid[ch_idx] if ch_idx < len(s3m_grid) else [S3MRow()] * PATTERN_ROWS
last_inst = 0 # 1-based; tracks which instrument is loaded on this channel last_inst = 0 # 1-based; tracks which instrument is loaded on this channel
last_note = S3M_NOTE_EMPTY # last raw S3M note byte that was a real pitch
last_vol = None # last SEL_SET volume value (0-63), for retrigger recall
for r, row in enumerate(rows[:PATTERN_ROWS]): for r, row in enumerate(rows[:PATTERN_ROWS]):
note = encode_note(row.note) note = encode_note(row.note)
inst = row.inst # S3M 1-based Taud 1-based inst = row.inst # S3M 1-based -> Taud 1-based
if row.inst > 0: if row.inst > 0:
last_inst = row.inst last_inst = row.inst
# ── Instrument-only retrigger ──
# Instrument-only row: recall the last volume without touching the note.
retrigger = (row.inst > 0
and row.note == S3M_NOTE_EMPTY
and last_note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF))
op, arg, vol_override, pan_override = encode_effect( op, arg, vol_override, pan_override = encode_effect(
row.effect, row.effect_arg, ch_idx, r, amiga_mode=amiga_mode) row.effect, row.effect_arg, ch_idx, r, amiga_mode=amiga_mode)
# ── Volume column ── # -- Volume column --
note_triggers = (row.note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF))
if row.vol >= 0: if row.vol >= 0:
vol_sel, vol_value = SEL_SET, min(row.vol, 0x3F) vol_sel, vol_value = SEL_SET, min(row.vol, 0x3F)
if vol_override is not None and vol_override[0] != SEL_SET: if vol_override is not None and vol_override[0] != SEL_SET:
vprint(f" ch{ch_idx} row{r}: dropped vol slide " vprint(f" ch{ch_idx} row{r}: dropped vol slide "
f"(cell already carries explicit volume)") f"(cell already carries explicit volume)")
elif note_triggers and row.inst > 0:
# Note trigger with a fresh instrument: use that instrument's
# default volume.
vol_sel = SEL_SET
vol_value = inst_vols.get(last_inst, 0x3F)
elif note_triggers and last_vol is not None:
# Note trigger without instrument: keep the channel's current
# volume rather than resetting to the instrument default.
vol_sel, vol_value = SEL_SET, last_vol
elif retrigger and last_vol is not None:
# Instrument-only row: re-emit the last known volume so the sample
# restarts at the correct level without an explicit note trigger.
vol_sel, vol_value = SEL_SET, last_vol
elif vol_override is not None: elif vol_override is not None:
vol_sel, vol_value = vol_override vol_sel, vol_value = vol_override
else: else:
vol_sel, vol_value = SEL_FINE, 0 # no-op fine slide vol_sel, vol_value = SEL_FINE, 0 # no-op fine slide
# Track note and volume for future retrigger lookups.
if row.note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF):
last_note = row.note
if vol_sel == SEL_SET:
last_vol = vol_value
# ── Pan column ── # ── Pan column ──
if pan_override is not None: if pan_override is not None:
pan_sel, pan_value = pan_override pan_sel, pan_value = pan_override
@@ -831,7 +806,9 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
# Song table row (32 bytes; see encode_song_entry). # Song table row (32 bytes; see encode_song_entry).
# flags byte: bit 1 (f) = Amiga pitch-slide mode (mirrors the S3M linear_slides flag inverted) # flags byte: bit 1 (f) = Amiga pitch-slide mode (mirrors the S3M linear_slides flag inverted)
flags_byte = 0x00 if h.linear_slides else 0x02 # bit 2 (m) set: FT2 fadeout-zero policy — S3M has no per-instrument fadeout field, so a
# stored zero means "cut on key-off" (matching ST3's lineage from the FT2 family).
flags_byte = (0x00 if h.linear_slides else 0x02) | 0x04
song_table = encode_song_entry( song_table = encode_song_entry(
song_offset=song_offset, song_offset=song_offset,
num_voices=C, num_voices=C,
@@ -844,7 +821,7 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
pat_bin_comp_size=len(pat_comp), pat_bin_comp_size=len(pat_comp),
cue_sheet_comp_size=len(cue_comp), cue_sheet_comp_size=len(cue_comp),
global_vol=0xFF, global_vol=0xFF,
mixing_vol=0xFF, mixing_vol=180,
) )
assert len(song_table) == TAUD_SONG_ENTRY assert len(song_table) == TAUD_SONG_ENTRY

View File

@@ -1995,30 +1995,29 @@ Memory Space
Sample bin: just raw sample data thrown in there. You need to keep track of starting point for each sample Sample bin: just raw sample data thrown in there. You need to keep track of starting point for each sample
Instrument bin: Registry for 256 instruments, formatted as: Instrument bin: Registry for 256 instruments, formatted as:
Uint32 Sample Pointer 0 Uint32 Sample Pointer
Uint16 Sample length 4 Uint16 Sample length
Uint16 Sampling rate at C4 (note number 0x5000) 6 Uint16 Sampling rate at C4 (note number 0x5000)
Uint16 Play Start (usually 0 but not always) 8 Uint16 Play Start (usually 0 but not always)
Uint16 Loop Start (can be smaller than Play Start) 10 Uint16 Loop Start (can be smaller than Play Start)
Uint16 Loop End 12 Uint16 Loop End
Bit8 Sample Flags 14 Bit8 Sample Flags
0b 0000 0spp 0b 0000 0spp
pp: loop mode. 0-no loop, 1-loop, 2-backandforth, 3-oneshot (ignores note length unless overridden by other notes) 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) s: loop is sustain (key-off escapes the loop)
- IT: look for sample's SusLoop flag - IT: look for sample's SusLoop flag
Bit16 Volume envelope sustain/loops and flags 15 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 * 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 0b 0ut sssss 0cb eeeee
s: sustain/loop start index s: sustain/loop start index
e: sustain/loop end index e: sustain/loop end index
b: use envelope b: use envelope
c: envelope carry c: envelope carry
p: (IT) fadeout is zero; (XM) fadeout is cut
t: the loop must sustain (key-off escapes the loop) t: the loop must sustain (key-off escapes the loop)
u: set to enable the sustain/loop u: set to enable the sustain/loop
Bit16 Panning envelope sustain/loops and flags 17 Bit16 Panning envelope sustain/loops and flags
* Sustain is implemented by enabling 't' flag * Sustain is implemented by enabling 't' flag
0b 0ut sssss pcb eeeee 0b 0ut sssss pcb eeeee
s: sustain/loop start index s: sustain/loop start index
@@ -2026,11 +2025,11 @@ Instrument bin: Registry for 256 instruments, formatted as:
b: use envelope b: use envelope
c: envelope carry c: envelope carry
p: use default pan (see offset 176 "Default pan value" below) p: use default pan (see offset 177 "Default pan value" below)
t: the loop must sustain (key-off escapes the loop) t: the loop must sustain (key-off escapes the loop)
u: set to enable the sustain/loop u: set to enable the sustain/loop
Bit16 Pitch/Filter envelope sustain/loops and flags 19 Bit16 Pitch/Filter envelope sustain/loops and flags
* Sustain is implemented by enabling 't' flag * Sustain is implemented by enabling 't' flag
0b 0ut sssss mcb eeeee 0b 0ut sssss mcb eeeee
s: sustain/loop start index s: sustain/loop start index
@@ -2042,48 +2041,57 @@ Instrument bin: Registry for 256 instruments, formatted as:
t: the loop must sustain (key-off escapes the loop) t: the loop must sustain (key-off escapes the loop)
u: set to enable the sustain/loop u: set to enable the sustain/loop
Bit16x25 Volume envelopes 21 Bit16x25 Volume envelopes
Byte 1: Volume (00..3F) Byte 1: Volume (00..3F)
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely. Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
Bit16x25 Panning envelopes 71 Bit16x25 Panning envelopes
Byte 1: Pan (00..FF) Byte 1: Pan (00..FF)
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely. Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
Bit16x25 Pitch/Filter envelopes 121 Bit16x25 Pitch/Filter envelopes
Byte 1: Value (00..FF) Byte 1: Value (00..FF)
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely. 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) 171 Uint8 Instrument Global Volume (0..255)
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int * ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
- ImpulseTracker also 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 - ImpulseTracker also 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 * 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) 172 Uint8 Volume Fadeout low bits
Bit8 Fadeout and vibrato 173 Bit8 Fadeout and vibrato
0b 0000 ffff 0b 0000 ffff
f: Volume Fadeout high bits f: Volume Fadeout high bits
Uint8 Volume swing (0..255 full range) * Combined 12-bit fadeout value is the engine's per-tick decrement, in 1/65536 units
Uint8 Vibrato speed (a unity-volume voice silenced over (65536 / fadeout) ticks after key-off).
* Stored 0: behaviour depends on Global Behaviour bit 'm' (see Song Table) —
IT mode (m=0) leaves the voice unfaded; FT2 mode (m=1) cuts on key-off.
* Source-format mapping:
- IT: stored fadeout (0..1024) MUST be doubled on import (taud = it × 2);
Taud's per-tick scale matches FT2 natively, so IT values are scaled to match.
- FT2: stored fadeout (0..0xFFF) is passed through unchanged.
174 Uint8 Volume swing (0..255 full range)
175 Uint8 Vibrato speed
* ImpulseTracker has samplewise vibrato speed (0..64), and they must be taken into account because Taud has no samplewise config * 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) * FastTracker2 has instrumentwise config (0..255)
* The spec follows FastTracker2, and conversion must be performed when importing from FastTracker2 * The spec follows FastTracker2, and conversion must be performed when importing from FastTracker2
Uint8 Vibrato sweep 176 Uint8 Vibrato sweep
* FastTracker2 instrument config * FastTracker2 instrument config
Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag) 177 Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag)
* ImpulseTracker has samplewise default pan and instrumentwise default pan, and they must be taken into account because Taud has no samplewise config * ImpulseTracker has samplewise default pan and instrumentwise default pan, and they must be taken into account because Taud has no samplewise config
Uint16 Pitch-pan centre (4096-TET note value) 178 Uint16 Pitch-pan centre (4096-TET note value)
Sint8 Pitch-pan separation (-128..127 full range) 180 Sint8 Pitch-pan separation (-128..127 full range)
Uint8 Pan swing (0..255 full range) 181 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) 182 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) 183 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) 184 Uint16 Sample detune (in 4096-TET unit) (FT2 finetune scale need to be rescaled accordingly)
Bit8 Instrument Flag 186 Bit8 Instrument Flag
0b 000 www nn 0b 000 www nn
n: New note action. 00: note off, 01: note cut, 10: continue, 11: note fade (arranged differently to IT) 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) 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) 187 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 * 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 * FastTracker2 has range of 0..16; multiply by (255/16) then round to int
Uint8 Vibrato Rate (0..255 full range) 188 Uint8 Vibrato Rate (0..255 full range)
* ImpulseTracker sample config. The spec follows ImpulseTracker precisely * ImpulseTracker sample config. The spec follows ImpulseTracker precisely
Byte[4] Reserved 189 Byte[3] Reserved
TODO: TODO:
@@ -2098,8 +2106,14 @@ TODO:
[x] implement sample loop sustain [x] implement sample loop sustain
"Caveat: on a foreground voice, key-off (row.note == 0x0000) currently sets voice.active = false at AudioAdapter.kt:1713, which silences the channel immediately. Sustain-loop escape therefore only takes effect on background voices spawned by NNA "Note Off" — which matches the IT idiom of layering a new note over a sustained one. Let me know if you also want the foreground key-off to keep the voice playing through fadeout." "Caveat: on a foreground voice, key-off (row.note == 0x0000) currently sets voice.active = false at AudioAdapter.kt:1713, which silences the channel immediately. Sustain-loop escape therefore only takes effect on background voices spawned by NNA "Note Off" — which matches the IT idiom of layering a new note over a sustained one. Let me know if you also want the foreground key-off to keep the voice playing through fadeout."
[x] cue and pattern compression of the Taud format (taud_common.py, taud.mjs) [x] 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 [x] figure out how IT (0..256) and FT2 (0..FFF + cut) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
[ ] implement bitcrusher (eff sym '8') [x] Pitchbend on Amiga frequency mode sometimes works right, sometimes works wrong. (effect underdelivers) Affects every song with Amiga picth mode, AND ON THE fresh taut.js session only
[x] Fix 4THSYM.it filters
[x] 4THSYM.it: pitchbend is wrong, some notes keep playing (loudly!) even if new notes are emitted
[x] `*2taud.py`: some notes are emitted with wrong volume-set command. Tested with GSLINGER.mod: on order 0x15 channel 1, mod2taud.py emits volume 8 -- also many of the effects are dropped. Suggested solution: currently all converters write default volume to the voleff when original modules (.mod/.s3m/.it) specify nothing; we should also write nothing and let the engine resolve the value just like other trackers do (also we now have "Instrument Global Volume" on instrument definition unlike the other time). This bug may affecting other formats, not just mod2taud.py, as well
[x] nearly_there_.mod: `C#5 SD300 / ... / C-5 SD200 / A#4 / G#4 (at tickspeed 4)`: every `C-5 SD200` (there are four occurances) gets skipped
[ ] scale Oxxxx when samples get resampled
[ ] implement bitcrusher and overdrive (eff sym '8' and '9')
Play Data: play data are series of tracker-like instructions, visualised as: Play Data: play data are series of tracker-like instructions, visualised as:
@@ -2314,6 +2328,11 @@ Endianness: Little
Uint16 Current Tuning base note (1..65533). A4 (western default) is 0x5C00. C9 (tracker default) is 0xA000. If zero, assume the tracker default value Uint16 Current Tuning base note (1..65533). A4 (western default) is 0x5C00. C9 (tracker default) is 0xA000. If zero, assume the tracker default value
Float32 Frequency at the base note. Tracker default is 8363.0. If zero, assume the tracker default Float32 Frequency at the base note. Tracker default is 8363.0. If zero, assume the tracker default
Uint8 Flags for Global Behaviour (effect symbol '1') Uint8 Flags for Global Behaviour (effect symbol '1')
0b 0000 0mfp
p: panning law (0=linear, 1=equal-power)
f: tone mode (0=linear pitch slides, 1=Amiga period slides)
m: fadeout-zero policy (0=IT — stored fadeout 0 means no fadeout;
1=FT2 — stored fadeout 0 means cut on key-off)
Uint8 Song global volume Uint8 Song global volume
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int * ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
Uint8 Song mixing volume Uint8 Song mixing volume

View File

@@ -132,7 +132,14 @@ class AudioJSR223Delegate(private val vm: VM) {
} }
fun setTrackerMixerFlags(playhead: Int, flags: Int) { fun setTrackerMixerFlags(playhead: Int, flags: Int) {
getFirstSnd()?.playheads?.get(playhead)?.initialGlobalFlags = flags getFirstSnd()?.playheads?.get(playhead)?.let { ph ->
ph.initialGlobalFlags = flags
ph.trackerState?.let { ts ->
ts.panLaw = flags and 1
ts.amigaMode = (flags and 2) != 0
ts.fadeoutCutOnZero = (flags and 4) != 0
}
}
} }
fun getTrackerMixerFlags(playhead: Int): Int? { fun getTrackerMixerFlags(playhead: Int): Int? {

View File

@@ -1170,15 +1170,36 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
inst.samplingRate.toDouble() / SAMPLING_RATE * inst.samplingRate.toDouble() / SAMPLING_RATE *
2.0.pow((noteVal - MIDDLE_C + inst.sampleDetuneSigned) / 4096.0) 2.0.pow((noteVal - MIDDLE_C + inst.sampleDetuneSigned) / 4096.0)
// Convert a 4096-TET noteVal to its Amiga-period equivalent (Double, no rounding).
private fun noteValToAmigaPeriod(noteVal: Int): Double =
AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - MIDDLE_C).toDouble() / 4096.0)
// Convert an Amiga period (Double) to the nearest 4096-TET noteVal.
private fun amigaPeriodToNoteVal(period: Double): Int =
(MIDDLE_C + 4096.0 * log2(AMIGA_BASE_PERIOD / period)).roundToInt()
// Applies one tick of Amiga-mode pitch slide. When the song is in Amiga tone mode, E/F coarse // 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), // slide arguments are stored as raw tracker period units (the original ProTracker/ST3 byte),
// *not* scaled to 4096-TET — see TAUD_NOTE_EFFECTS.md §1 and §E/F. Sign convention matches // *not* scaled to 4096-TET — see TAUD_NOTE_EFFECTS.md §1 and §E/F. Sign convention matches
// linear mode: negative = pitch down (E effect), positive = pitch up (F effect), so a positive // linear mode: negative = pitch down (E effect), positive = pitch up (F effect), so a positive
// slideArg subtracts from the period (pitch rises). // slideArg subtracts from the period (pitch rises).
private fun amigaSlide(noteVal: Int, slideArg: Int): Int { //
val period = AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - MIDDLE_C).toDouble() / 4096.0) // Period state is persisted on the Voice (voice.amigaPeriod) so accumulated period changes
// don't lose sub-noteVal precision via repeated noteVal-int rounding. voice.amigaPeriod < 0
// means the cache is stale and must be reseeded from the current noteVal.
private fun amigaSlideTick(voice: Voice, slideArg: Int): Int {
if (voice.amigaPeriod < 0.0) voice.amigaPeriod = noteValToAmigaPeriod(voice.noteVal)
voice.amigaPeriod = (voice.amigaPeriod - slideArg).coerceAtLeast(1.0)
return amigaPeriodToNoteVal(voice.amigaPeriod)
}
// One-shot Amiga slide that does NOT mutate persistent period state — used for
// fine slides (EFx / FFx) which are applied once per row at tick 0. The next
// multi-tick slide will reseed amigaPeriod from the resulting noteVal.
private fun amigaSlideOnce(noteVal: Int, slideArg: Int): Int {
val period = noteValToAmigaPeriod(noteVal)
val newPeriod = (period - slideArg).coerceAtLeast(1.0) val newPeriod = (period - slideArg).coerceAtLeast(1.0)
return (MIDDLE_C + 4096.0 * log2(AMIGA_BASE_PERIOD / newPeriod)).roundToInt() return amigaPeriodToNoteVal(newPeriod)
} }
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) { private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
@@ -1448,8 +1469,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (i0 in ls until inst.sampleLoopEnd && inst.funkBit(i0 - ls)) b0 = b0 xor 0x80 if (i0 in ls until inst.sampleLoopEnd && inst.funkBit(i0 - ls)) b0 = b0 xor 0x80
if (i1 in ls until inst.sampleLoopEnd && inst.funkBit(i1 - ls)) b1 = b1 xor 0x80 if (i1 in ls until inst.sampleLoopEnd && inst.funkBit(i1 - ls)) b1 = b1 xor 0x80
} }
val s0 = (b0 - 128) / 128.0 val s0 = (b0 - 127.5) / 127.5
val s1 = (b1 - 128) / 128.0 val s1 = (b1 - 127.5) / 127.5
val sample = s0 + (s1 - s0) * frac val sample = s0 + (s1 - s0) * frac
if (voice.forward) { if (voice.forward) {
@@ -1530,10 +1551,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.filterResonanceCached = -1 voice.filterResonanceCached = -1
voice.noteVal = noteVal voice.noteVal = noteVal
voice.basePitch = noteVal voice.basePitch = noteVal
voice.amigaPeriod = -1.0 // fresh trigger: period state must reseed from the new noteVal
voice.playbackRate = computePlaybackRate(inst, noteVal) voice.playbackRate = computePlaybackRate(inst, noteVal)
if (volOverride >= 0) { // Fresh trigger resets channel volume to full ($3F). Per-instrument scaling lives in
voice.channelVolume = volOverride.coerceIn(0, 0x3F) // instGlobalVolume (byte 171), which the mixer applies as a multiplier. Converters
} // therefore no longer need to emit SEL_SET=Sv on note-trigger rows.
voice.channelVolume = if (volOverride >= 0) volOverride.coerceIn(0, 0x3F) else 0x3F
voice.rowVolume = voice.channelVolume voice.rowVolume = voice.channelVolume
voice.noteWasCut = false voice.noteWasCut = false
voice.noteFading = false voice.noteFading = false
@@ -1548,6 +1571,63 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.panbrelloRetrig) voice.panbrelloLfoPos = 0 if (voice.panbrelloRetrig) voice.panbrelloLfoPos = 0
} }
/**
* IT-style Duplicate Check (DCT/DCA). Runs *before* NNA on every fresh foreground
* trigger: existing voices on this channel — the foreground itself plus any of its
* own background ghosts — that match the new note under DCT have DCA applied.
* Reference: schismtracker effects.c:1664-1764 (csf_check_nna).
*
* DCT (per existing voice's instrument):
* 1 = note — same noteVal AND same instrumentId
* 2 = sample — same canonical sample (matched by samplePtr+sampleLength)
* 3 = instrument — same instrumentId
* DCA: 0 = note cut, 1 = note off (release sustain), 2 = note fade.
*
* Note: the foreground voice will be replaced by triggerNote() right after this,
* so applying DCA to it is mostly relevant for ghosts spawned *from* it via NNA
* — the ghost is cloned from the (already-DCA-modified) foreground state.
*/
private fun applyDuplicateCheck(ts: TrackerState, channel: Int, newInstId: Int, newNote: Int) {
if (newInstId == 0) return
val newInst = instruments[newInstId]
fun isDuplicate(v: Voice): Boolean {
val existInst = instruments[v.instrumentId]
return when (existInst.duplicateCheckType) {
1 -> v.noteVal == newNote && v.instrumentId == newInstId
2 -> v.instrumentId == newInstId &&
existInst.samplePtr == newInst.samplePtr &&
existInst.sampleLength == newInst.sampleLength
3 -> v.instrumentId == newInstId
else -> false
}
}
fun applyAction(v: Voice) {
val existInst = instruments[v.instrumentId]
when (existInst.duplicateCheckAction) {
0 -> { v.fadeoutVolume = 0.0; v.active = false }
1 -> v.keyOff = true
2 -> v.noteFading = true
}
}
val fg = ts.voices[channel]
if (fg.active && instruments[fg.instrumentId].duplicateCheckType != 0 && isDuplicate(fg)) {
applyAction(fg)
}
val it = ts.backgroundVoices.iterator()
while (it.hasNext()) {
val bg = it.next()
if (bg.sourceChannel != channel || !bg.active) continue
if (instruments[bg.instrumentId].duplicateCheckType == 0) continue
if (!isDuplicate(bg)) continue
applyAction(bg)
if (!bg.active) it.remove()
}
}
/** /**
* On a fresh foreground trigger, optionally migrate the existing voice into the * On a fresh foreground trigger, optionally migrate the existing voice into the
* mixer-private background pool per the New Note Action setting (instrument default * mixer-private background pool per the New Note Action setting (instrument default
@@ -1616,6 +1696,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
v.randomPanBias = src.randomPanBias v.randomPanBias = src.randomPanBias
v.noteVal = src.noteVal v.noteVal = src.noteVal
v.basePitch = src.basePitch v.basePitch = src.basePitch
v.amigaPeriod = src.amigaPeriod
v.volEnvOn = src.volEnvOn v.volEnvOn = src.volEnvOn
v.panEnvOn = src.panEnvOn v.panEnvOn = src.panEnvOn
v.pfEnvOn = src.pfEnvOn v.pfEnvOn = src.pfEnvOn
@@ -1713,7 +1794,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// ── Note ── // ── Note ──
val toneG = (row.effect == EffectOp.OP_G) val toneG = (row.effect == EffectOp.OP_G)
when (row.note) { when (row.note) {
0xFFFF -> {} // no-op // No note but an instrument byte is present: latch the instrument so
// the *next* note-only trigger picks up the right sample. Trackers
// call this an "instrument-only retrigger"; in MOD/S3M/IT the sample
// keeps playing, but the channel's instrument reference advances.
0xFFFF -> { if (row.instrment != 0) voice.instrumentId = row.instrment }
0x0000 -> { voice.keyOff = true; voice.active = false } // key-off; breaks sustain loop 0x0000 -> { voice.keyOff = true; voice.active = false } // key-off; breaks sustain loop
0xFFFE -> voice.active = false // note cut 0xFFFE -> voice.active = false // note cut
else -> { else -> {
@@ -1726,8 +1811,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.noteDelayTick = (row.effectArg ushr 8) and 0xF voice.noteDelayTick = (row.effectArg ushr 8) and 0xF
voice.delayedNote = row.note voice.delayedNote = row.note
voice.delayedInst = row.instrment voice.delayedInst = row.instrment
voice.delayedVol = if (row.volume >= 0) row.volume else -1 // Only treat the vol cell as an override when it carries SEL_SET;
// SEL_FINE/0 (no-op) and slide selectors must not collapse into
// a SET=0 on the deferred trigger.
voice.delayedVol = if (row.volumeEff == 0) row.volume else -1
} else { } else {
applyDuplicateCheck(ts, vi, row.instrment, row.note)
maybeSpawnBackgroundForNNA(ts, voice, vi) maybeSpawnBackgroundForNNA(ts, voice, vi)
triggerNote(voice, row.note, row.instrment, -1) triggerNote(voice, row.note, row.instrment, -1)
} }
@@ -1754,9 +1843,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1). // 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1).
// bit 0 (p): 0=linear pan, 1=equal-power pan // bit 0 (p): 0=linear pan, 1=equal-power pan
// bit 1 (f): 0=linear pitch slides, 1=Amiga-mode pitch slides // bit 1 (f): 0=linear pitch slides, 1=Amiga-mode pitch slides
// bit 2 (m): fadeout-zero policy. 0=IT (stored 0 ⇒ no fadeout), 1=FT2 (stored 0 ⇒ cut on key-off)
val flags = rawArg ushr 8 val flags = rawArg ushr 8
ts.panLaw = flags and 1 ts.panLaw = flags and 1
ts.amigaMode = (flags and 2) != 0 ts.amigaMode = (flags and 2) != 0
ts.fadeoutCutOnZero = (flags and 4) != 0
} }
EffectOp.OP_A -> { EffectOp.OP_A -> {
val tr = (rawArg ushr 8) and 0xFF val tr = (rawArg ushr 8) and 0xFF
@@ -1787,13 +1878,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if ((arg and 0xF000) == 0xF000) { if ((arg and 0xF000) == 0xF000) {
val mag = arg and 0x0FFF val mag = arg and 0x0FFF
voice.noteVal = if (ts.amigaMode) voice.noteVal = if (ts.amigaMode)
amigaSlide(voice.noteVal, -mag).coerceIn(0, 0xFFFE) amigaSlideOnce(voice.noteVal, -mag).coerceIn(0, 0xFFFE)
else else
(voice.noteVal - mag).coerceIn(0, 0xFFFE) (voice.noteVal - mag).coerceIn(0, 0xFFFE)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal) voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal)
} else { } else {
voice.slideMode = 1; voice.slideArg = -arg voice.slideMode = 1; voice.slideArg = -arg
voice.amigaPeriod = -1.0 // reseed at the start of a fresh multi-tick slide
} }
} }
EffectOp.OP_F -> { EffectOp.OP_F -> {
@@ -1801,13 +1894,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if ((arg and 0xF000) == 0xF000) { if ((arg and 0xF000) == 0xF000) {
val mag = arg and 0x0FFF val mag = arg and 0x0FFF
voice.noteVal = if (ts.amigaMode) voice.noteVal = if (ts.amigaMode)
amigaSlide(voice.noteVal, mag).coerceIn(0, 0xFFFE) amigaSlideOnce(voice.noteVal, mag).coerceIn(0, 0xFFFE)
else else
(voice.noteVal + mag).coerceIn(0, 0xFFFE) (voice.noteVal + mag).coerceIn(0, 0xFFFE)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0
voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal) voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal)
} else { } else {
voice.slideMode = 2; voice.slideArg = arg voice.slideMode = 2; voice.slideArg = arg
voice.amigaPeriod = -1.0
} }
} }
EffectOp.OP_G -> { EffectOp.OP_G -> {
@@ -1922,6 +2017,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
0x2 -> { 0x2 -> {
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(0, 0xFFFE) voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(0, 0xFFFE)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0
voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal) voice.playbackRate = computePlaybackRate(instruments[voice.instrumentId], voice.noteVal)
} }
0x3 -> { voice.vibratoWave = x and 3; voice.vibratoRetrig = (x and 4) == 0 } 0x3 -> { voice.vibratoWave = x and 3; voice.vibratoRetrig = (x and 4) == 0 }
@@ -1997,6 +2093,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Note delay — fire deferred trigger when the requested tick arrives. // Note delay — fire deferred trigger when the requested tick arrives.
// NNA fires now (not at row parse) so that delayed retriggers ghost correctly. // NNA fires now (not at row parse) so that delayed retriggers ghost correctly.
if (voice.noteDelayTick == ts.tickInRow) { if (voice.noteDelayTick == ts.tickInRow) {
applyDuplicateCheck(ts, vi, voice.delayedInst, voice.delayedNote)
maybeSpawnBackgroundForNNA(ts, voice, vi) maybeSpawnBackgroundForNNA(ts, voice, vi)
triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol) triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol)
voice.noteDelayTick = -1 voice.noteDelayTick = -1
@@ -2007,7 +2104,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Pitch slides (E/F coarse on tick > 0). // Pitch slides (E/F coarse on tick > 0).
if (ts.tickInRow > 0 && (voice.slideMode == 1 || voice.slideMode == 2)) { if (ts.tickInRow > 0 && (voice.slideMode == 1 || voice.slideMode == 2)) {
voice.noteVal = if (ts.amigaMode) voice.noteVal = if (ts.amigaMode)
amigaSlide(voice.noteVal, voice.slideArg).coerceIn(0, 0xFFFE) amigaSlideTick(voice, voice.slideArg).coerceIn(0, 0xFFFE)
else else
(voice.noteVal + voice.slideArg).coerceIn(0, 0xFFFE) (voice.noteVal + voice.slideArg).coerceIn(0, 0xFFFE)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
@@ -2023,6 +2120,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.noteVal = target; voice.tonePortaTarget = -1 voice.noteVal = target; voice.tonePortaTarget = -1
} }
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 // tone porta works in linear noteVal space; reseed period
} }
// Volume slides (D coarse on tick > 0). // Volume slides (D coarse on tick > 0).
@@ -2121,32 +2219,48 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Auto-vibrato (instrument-supplied sample LFO) — added on top of pitchToMixer. // Auto-vibrato (instrument-supplied sample LFO) — added on top of pitchToMixer.
val autoVibDelta = advanceAutoVibrato(voice, inst) val autoVibDelta = advanceAutoVibrato(voice, inst)
// Pitch envelope contribution: env value 0..1, 0.5 = unity. -32..+32 // Pitch envelope contribution: env value 0..1, 0.5 = unity.
// semitone range maps to ±32 × 4096/12 ≈ ±10923 4096-TET units. // IT pitch envelope max is ±16 semitones (Schism sndmix.c:455-462 indexes
// linear_slide_up_table[abs(envpitch)] where envpitch ∈ [-256,+256] and
// table[255] = 65536·2^(255/192) ≈ 2.504, i.e. 15.94 semitones).
val pitchEnvDelta = if (voice.hasPfEnv && voice.pfEnvOn && !voice.envPfIsFilter) val pitchEnvDelta = if (voice.hasPfEnv && voice.pfEnvOn && !voice.envPfIsFilter)
((voice.envPfValue - 0.5) * 2.0 * 32.0 * 4096.0 / 12.0).toInt() ((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
else 0 else 0
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE) val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE)
voice.playbackRate = computePlaybackRate(inst, finalPitch) voice.playbackRate = computePlaybackRate(inst, finalPitch)
// Filter envelope (filter mode): scale current cutoff by env value (0..1, 0.5 = unity). // Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
// Schism filters.c:80-86 computes `cutoff_used = chan->cutoff * (flt_modifier+256)/256`
// where flt_modifier = (env_value_0..64 - 32) * 8. Mapping TSVM's [0..1] env to Schism's
// [-256..+256] modifier and accounting for our pre-doubled defaultCutoff (it2taud.py
// stores IFC*2 in 0..254) gives `currentCutoff = baseCut * envPfValue` — at unity (0.5)
// the filter sits at IFC, at max (1.0) it opens to 2*IFC, at min (0.0) it closes.
// If the instrument has no initial cutoff (255 = off), the envelope drives the filter // 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. // from the maximum active value (254) so the filter can become audible during the note.
if (voice.hasPfEnv && voice.pfEnvOn && voice.envPfIsFilter) { if (voice.hasPfEnv && voice.pfEnvOn && voice.envPfIsFilter) {
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254 val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
voice.currentCutoff = (baseCut * (voice.envPfValue * 2.0)).toInt().coerceIn(0, 254) voice.currentCutoff = (baseCut * voice.envPfValue).toInt().coerceIn(0, 254)
} }
// Refresh biquad filter coefficients once per tick (only recomputes when changed). // Refresh biquad filter coefficients once per tick (only recomputes when changed).
refreshVoiceFilter(voice) refreshVoiceFilter(voice)
// Volume fadeout: after key-off OR Note-Fade NNA, decrement by inst.volumeFadeout / 1024 per tick. // Volume fadeout: after key-off OR Note-Fade NNA, decrement per tick.
// The 10-bit fadeout value is split across volumeFadeoutLow + low nibble of fadeoutHigh. // The 12-bit fadeStep is split across volumeFadeoutLow + low nibble of fadeoutHigh.
// Divisor selects per-tracker semantics:
// FT2 mode (fadeoutCutOnZero=true): fadeStep / 65536 per tick — matches FT2 .XM (16-bit accumulator, decrement = stored).
// IT mode (fadeoutCutOnZero=false): fadeStep / 1024 per tick — matches Schism (sndmix.c:331-339 + effects.c:1261:
// accumulator 65536, decrement = (stored<<5)<<1 = stored·64).
// Stored 0: FT2 mode cuts on key-off; IT mode leaves voice playing (no fade).
if (voice.keyOff || voice.noteFading) { if (voice.keyOff || voice.noteFading) {
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8) val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8)
if (fadeStep > 0) { if (fadeStep > 0) {
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / 1024.0).coerceAtLeast(0.0) val divisor = if (ts.fadeoutCutOnZero) 65536.0 else 1024.0
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / divisor).coerceAtLeast(0.0)
if (voice.fadeoutVolume <= 0.0) voice.active = false
} else if (ts.fadeoutCutOnZero) {
voice.active = false
} }
} }
@@ -2198,20 +2312,25 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (bg.keyOff || bg.noteFading) { if (bg.keyOff || bg.noteFading) {
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8) val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8)
if (fadeStep > 0) { if (fadeStep > 0) {
bg.fadeoutVolume = (bg.fadeoutVolume - fadeStep / 1024.0).coerceAtLeast(0.0) val divisor = if (ts.fadeoutCutOnZero) 65536.0 else 1024.0
bg.fadeoutVolume = (bg.fadeoutVolume - fadeStep / divisor).coerceAtLeast(0.0)
} else if (ts.fadeoutCutOnZero) {
bg.active = false
bgIt.remove()
continue
} }
} }
// Auto-vibrato keeps running on backgrounds — it's an instrument-intrinsic LFO. // Auto-vibrato keeps running on backgrounds — it's an instrument-intrinsic LFO.
val autoVibDelta = advanceAutoVibrato(bg, inst) val autoVibDelta = advanceAutoVibrato(bg, inst)
val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter) val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
((bg.envPfValue - 0.5) * 2.0 * 32.0 * 4096.0 / 12.0).toInt() ((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
else 0 else 0
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE) val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE)
bg.playbackRate = computePlaybackRate(inst, finalPitch) bg.playbackRate = computePlaybackRate(inst, finalPitch)
// Filter-mode pf envelope: same scaling rule as foreground. // Filter-mode pf envelope: same scaling rule as foreground.
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) { if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254 val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
bg.currentCutoff = (baseCut * (bg.envPfValue * 2.0)).toInt().coerceIn(0, 254) bg.currentCutoff = (baseCut * bg.envPfValue).toInt().coerceIn(0, 254)
} }
refreshVoiceFilter(bg) refreshVoiceFilter(bg)
// Reap fully-faded ghosts so the pool stays drained. // Reap fully-faded ghosts so the pool stays drained.
@@ -2252,6 +2371,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
} }
} }
private fun Double.sqrt() = Math.sqrt(this)
internal fun generateTrackerAudio(playhead: Playhead): ByteArray? { internal fun generateTrackerAudio(playhead: Playhead): ByteArray? {
val ts = playhead.trackerState ?: return null val ts = playhead.trackerState ?: return null
@@ -2289,7 +2410,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val swingScale = 1.0 + voice.randomVolBias / 255.0 val swingScale = 1.0 + voice.randomVolBias / 255.0
// Volume envelope is bypassed (treated as unity) when S $77 has disabled it. // Volume envelope is bypassed (treated as unity) when S $77 has disabled it.
val effEnvVol = if (voice.volEnvOn) voice.envVolume else 1.0 val effEnvVol = if (voice.volEnvOn) voice.envVolume else 1.0
val vol = effEnvVol * voice.fadeoutVolume * voice.rowVolume / 63.0 * val vol = effEnvVol.sqrt() * voice.fadeoutVolume * (voice.rowVolume / 63.0).sqrt() *
swingScale * gvol * mvol * instGv * playhead.masterVolume / 255.0 swingScale * gvol * mvol * instGv * playhead.masterVolume / 255.0
val pan = if (voice.hasPanEnv && voice.panEnvOn) { val pan = if (voice.hasPanEnv && voice.panEnvOn) {
val envPanRaw = (voice.envPan * 255.0).roundToInt().coerceIn(0, 255) val envPanRaw = (voice.envPan * 255.0).roundToInt().coerceIn(0, 255)
@@ -2319,7 +2440,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val instGv = bgInst.instGlobalVolume / 255.0 val instGv = bgInst.instGlobalVolume / 255.0
val swingScale = 1.0 + bg.randomVolBias / 255.0 val swingScale = 1.0 + bg.randomVolBias / 255.0
val effEnvVol = if (bg.volEnvOn) bg.envVolume else 1.0 val effEnvVol = if (bg.volEnvOn) bg.envVolume else 1.0
val vol = effEnvVol * bg.fadeoutVolume * bg.rowVolume / 63.0 * val vol = effEnvVol.sqrt() * bg.fadeoutVolume * (bg.rowVolume / 63.0).sqrt() *
swingScale * gvol * mvol * instGv * playhead.masterVolume / 255.0 swingScale * gvol * mvol * instGv * playhead.masterVolume / 255.0
val pan = if (bg.hasPanEnv && bg.panEnvOn) { val pan = if (bg.hasPanEnv && bg.panEnvOn) {
val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255) val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255)
@@ -2568,6 +2689,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Pitch state (4096-TET units, signed when slid). // Pitch state (4096-TET units, signed when slid).
var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added) var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added)
var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay
// Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
// -1.0 means "needs reseed from current noteVal".
var amigaPeriod: Double = -1.0
// Per-row effect state (set in applyTrackerRow, consumed by applyTrackerTick). // Per-row effect state (set in applyTrackerRow, consumed by applyTrackerTick).
var rowEffect = 0 var rowEffect = 0
@@ -2662,6 +2787,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Global mixer config (effect 1). // Global mixer config (effect 1).
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
var amigaMode = false // false = linear pitch slides, true = Amiga period-space slides var amigaMode = false // false = linear pitch slides, true = Amiga period-space slides
var fadeoutCutOnZero = false // false = IT (stored 0 ⇒ no fadeout); true = FT2 (stored 0 ⇒ cut on key-off)
// Pending row-end events (set during a row by B/C; consumed at row end). // Pending row-end events (set during a row by B/C; consumed at row end).
var pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to var pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to
@@ -2772,7 +2898,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
} } } }
7 -> if (isPcmMode) { pcmUpload = true } else { 7 -> if (isPcmMode) { pcmUpload = true } else {
initialGlobalFlags = byte initialGlobalFlags = byte
trackerState?.let { ts -> ts.panLaw = byte and 1; ts.amigaMode = (byte and 2) != 0 } trackerState?.let { ts ->
ts.panLaw = byte and 1
ts.amigaMode = (byte and 2) != 0
ts.fadeoutCutOnZero = (byte and 4) != 0
}
} }
8 -> { bpm = byte + 24 } 8 -> { bpm = byte + 24 }
9 -> { tickRate = byte } 9 -> { tickRate = byte }
@@ -2807,6 +2937,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
ts.finePatternDelayExtra = 0 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.fadeoutCutOnZero = (initialGlobalFlags and 4) != 0
ts.voices.forEach { ts.voices.forEach {
it.active = false it.active = false
it.channelVolume = 0x3F it.channelVolume = 0x3F
@@ -2920,7 +3051,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
* waveform: 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2) * waveform: 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2)
* 187 u8 vibrato depth (0..255 full range) * 187 u8 vibrato depth (0..255 full range)
* 188 u8 vibrato rate (0..255 full range — IT samplewise Vir) * 188 u8 vibrato rate (0..255 full range — IT samplewise Vir)
* 189..191 byte[3] reserved * 189 u8 duplicate-check / action (IT-only — 0b 0000 aadd)
* dd = DCT (Duplicate Check Type) 0=off, 1=note, 2=sample, 3=instrument
* aa = DCA (Duplicate Check Action) 0=note cut, 1=note off, 2=note fade
* 190..191 byte[2] reserved
*/ */
data class TaudInst( data class TaudInst(
var index: Int, var index: Int,
@@ -2953,7 +3087,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var sampleDetune: Int, // bytes 184-185 (signed 4096-TET stored as u16) var sampleDetune: Int, // bytes 184-185 (signed 4096-TET stored as u16)
var instrumentFlag: Int, // byte 186 (NNA + vibrato waveform) var instrumentFlag: Int, // byte 186 (NNA + vibrato waveform)
var vibratoDepth: Int, // byte 187 (0..255 full range) var vibratoDepth: Int, // byte 187 (0..255 full range)
var vibratoRate: Int // byte 188 (IT samplewise Vir) var vibratoRate: Int, // byte 188 (IT samplewise Vir)
var dupCheckFlag: Int // byte 189 (DCT bits 0-1, DCA bits 2-3)
) { ) {
constructor(index: Int) : this( constructor(index: Int) : this(
index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
@@ -2961,7 +3096,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) }, Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) }, Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
0, 0, 0, 0, 0, 0x80, 0x5000, 0, 0, 0xFF, 0, 0, 0, 0, 0, 0, 0x80, 0x5000, 0, 0, 0xFF, 0,
0, 0, 0, 0 0, 0, 0, 0, 0
) )
/** Sample-flag byte 14 bit 2 — when set, the sample loop is a sustain loop: /** Sample-flag byte 14 bit 2 — when set, the sample loop is a sustain loop:
@@ -2975,9 +3110,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val vibratoWaveform: Int get() = (instrumentFlag ushr 2) and 0x07 val vibratoWaveform: Int get() = (instrumentFlag ushr 2) and 0x07
/** Sample detune as a signed 4096-TET delta. */ /** Sample detune as a signed 4096-TET delta. */
val sampleDetuneSigned: Int get() = sampleDetune.toShort().toInt() val sampleDetuneSigned: Int get() = sampleDetune.toShort().toInt()
/** Duplicate Check Type — 0=off, 1=note, 2=sample, 3=instrument (IT semantics). */
val duplicateCheckType: Int get() = dupCheckFlag and 0x03
/** Duplicate Check Action — 0=note cut, 1=note off, 2=note fade. */
val duplicateCheckAction: Int get() = (dupCheckFlag ushr 2) and 0x03
// Reserved padding at offsets 189..191 (3 bytes per instrument). // Reserved padding at offsets 190..191 (2 bytes per instrument).
private val reserved = ByteArray(3) private val reserved = ByteArray(2)
// Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region. // Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region.
// Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact. // Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact.
@@ -3059,7 +3198,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
186 -> instrumentFlag.toByte() 186 -> instrumentFlag.toByte()
187 -> vibratoDepth.toByte() 187 -> vibratoDepth.toByte()
188 -> vibratoRate.toByte() 188 -> vibratoRate.toByte()
in 189..191 -> reserved[offset - 189] 189 -> dupCheckFlag.toByte()
in 190..191 -> reserved[offset - 190]
else -> throw InternalError("Bad offset $offset") else -> throw InternalError("Bad offset $offset")
} }
@@ -3114,7 +3254,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
186 -> { instrumentFlag = byte and 0xFF } 186 -> { instrumentFlag = byte and 0xFF }
187 -> { vibratoDepth = byte and 0xFF } 187 -> { vibratoDepth = byte and 0xFF }
188 -> { vibratoRate = byte and 0xFF } 188 -> { vibratoRate = byte and 0xFF }
in 189..191 -> { reserved[offset - 189] = byte.toByte() } 189 -> { dupCheckFlag = byte and 0x0F } // DCT (bits 0-1) + DCA (bits 2-3)
in 190..191 -> { reserved[offset - 190] = byte.toByte() }
else -> throw InternalError("Bad offset $offset") else -> throw InternalError("Bad offset $offset")
} }
} }

View File

@@ -87,7 +87,7 @@ object OEMBios : VMProgramRom(File("./assets/bios/TBMBIOS.js"))
object QuickBios : VMProgramRom(File("./assets/bios/quick.js")) object QuickBios : VMProgramRom(File("./assets/bios/quick.js"))
object BasicBios : VMProgramRom(File("./assets/bios/basicbios.js")) object BasicBios : VMProgramRom(File("./assets/bios/basicbios.js"))
object TandemBios : VMProgramRom(File("./assets/bios/tandemport.js")) object TandemBios : VMProgramRom(File("./assets/bios/tandemport.js"))
object TsvmBios : VMProgramRom(File("./assets/bios/tsvmbios.bin")) object TsvmBios : VMProgramRom(File("./assets/bios/tsvmbios.js"))
object BasicRom : VMProgramRom(File("./assets/bios/basic.bin")) object BasicRom : VMProgramRom(File("./assets/bios/basic.bin"))
object WPBios : VMProgramRom(File("./assets/bios/wp.js")) object WPBios : VMProgramRom(File("./assets/bios/wp.js"))
object OpenBios : VMProgramRom(File("./assets/bios/openbios.js")) object OpenBios : VMProgramRom(File("./assets/bios/openbios.js"))

View File

@@ -576,7 +576,7 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
"assetsdir":"./assets", "assetsdir":"./assets",
"ramsize":8388608, "ramsize":8388608,
"cardslots":8, "cardslots":8,
"roms":["./assets/bios/tsvmbios.bin"], "roms":["./assets/bios/tsvmbios.js"],
"com1":{"cls":"net.torvald.tsvm.peripheral.TestDiskDrive", "args":[0, "./assets/disk0/"]}, "com1":{"cls":"net.torvald.tsvm.peripheral.TestDiskDrive", "args":[0, "./assets/disk0/"]},
"com2":{"cls":"net.torvald.tsvm.peripheral.HttpModem", "args":[1024, -1]}, "com2":{"cls":"net.torvald.tsvm.peripheral.HttpModem", "args":[1024, -1]},
"com3":{"cls":"net.torvald.tsvm.peripheral.TestDiskDrive", "args":[0, "./assets/diskMediabin/"]}, "com3":{"cls":"net.torvald.tsvm.peripheral.TestDiskDrive", "args":[0, "./assets/diskMediabin/"]},