From ed3bbb6ffe5a281430ff2086f2b5ff9caa621390 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Fri, 8 May 2026 20:23:46 +0900 Subject: [PATCH] Taud: equal-energy panning only --- TAUD_NOTE_EFFECTS.md | 14 ++---- assets/disk0/tvdos/bin/taut.js | 23 ++------- it2taud.py | 10 ++-- mod2taud.py | 12 ++--- mon2taud.py | 14 +++--- s3m2taud.py | 12 +++-- terranmon.txt | 12 ++--- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 8 +-- .../torvald/tsvm/peripheral/AudioAdapter.kt | 49 +++++-------------- xm2taud.py | 12 +++-- 10 files changed, 63 insertions(+), 103 deletions(-) diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index b9d47b5..21d1b4d 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -181,7 +181,7 @@ Coarse and fine modes are distinguished by the high nibble of the argument: - **MONOTONE source** (Taud `ff = 2`): - MONOTONE `2xx` → Taud `E $00xx` **verbatim** (Hz/tick). The engine converts the stored pitch to frequency, subtracts the argument, and converts back. MONOTONE has no fine-slide form; converters never emit `E $Fxxx` for ff=2 sources. -The mode flag therefore controls **two** decoder behaviours simultaneously: (a) which numeric scale the converter should have used when emitting coarse arguments, and (b) which arithmetic the engine performs on those arguments per tick. Converters MUST set bits 1-2 (`ff`) of the song-table flags byte to match the units they emit, and MUST NOT mix scales within one Taud song. +The mode flag therefore controls **two** decoder behaviours simultaneously: (a) which numeric scale the converter should have used when emitting coarse arguments, and (b) which arithmetic the engine performs on those arguments per tick. Converters MUST set bits 0-1 (`ff`) of the song-table flags byte to match the units they emit, and MUST NOT mix scales within one Taud song. Because E and F share memory in Taud (narrower than ST3's broad shared memory), an ST3 song that used `E00` or `F00` to recall a D, G, or Q argument will break on import; the converter must eagerly resolve ST3 recalls into explicit Taud arguments rather than relying on memory. @@ -1114,16 +1114,13 @@ Effects in this section modifies the behaviour of the mixer. Primary intention o **Plain.** Sets mixer-wide behaviour flags. Available flags are: - 0b 0000 0ffp - -- 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. + 0b 0000 00ff - ff = 0: 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. - ff = 1: 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. - ff = 2: Linear-frequency tone mode (MONOTONE compat). **E, F, and G arguments are stored as Hz/tick** (a signed change in audible frequency per song tick), and the engine converts the channel's stored 4096-TET pitch back to a frequency, adds/subtracts the argument, then converts back to 4096-TET. Reference is fixed at 12-TET A4 = 440 Hz / C4 ≈ 261.6256 Hz, which matches MONOTONE's MT_PLAY.PAS `notesHz` table (A0 = 27.5 Hz, equal-temperament). Unlike Amiga mode, *all three* slide effects use the new arithmetic — Monotone's `1xx`, `2xx`, and `3xx` are all in Hz/tick (see MTSRC/MT_PLAY.PAS:606-630). -(Bit 2 is reserved. It previously held an `m` "fadeout-zero policy" flag intended to swap between IT and FT2 semantics for `storedFadeout = 0`. That flag was removed once both trackers were verified to share identical "stored 0 ⇒ no fade" semantics — see schismtracker `player/sndmix.c:330-342` and ft2-clone `src/ft2_replayer.c:1467-1481`. Fadeout scaling now lives in the converters; see "Volume Fadeout" below.) +(Bits 2-7 are reserved. Bit 2 previously held an `m` "fadeout-zero policy" flag intended to swap between IT and FT2 semantics for `storedFadeout = 0`. That flag was removed once both trackers were verified to share identical "stored 0 ⇒ no fade" semantics — see schismtracker `player/sndmix.c:330-342` and ft2-clone `src/ft2_replayer.c:1467-1481`. Fadeout scaling now lives in the converters; see "Volume Fadeout" below.) ### Volume Fadeout @@ -1168,10 +1165,7 @@ There is no separate "use fadeout" flag — both extremes share the same field, - **MOD / S3M / MON**: source has no instrument-level fadeout. Converter writes Taud `0`. Notes retire on sample-end or pattern note-cut. **Implementation.** -- Panning-linear: - - L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0 - - R_gain = if (pan < 0x80) pan / 128.0 else 1.0 -- Panning-equal-power: +- Panning (equal-energy): - L_gain = cos(πx / 512.0) - R_gain = sin(πx / 512.0) - Amiga tone (both coarse and fine E/F pitch slides). The `slideArg` is a **raw tracker period-unit count** (no scaling), with sign matching linear mode (negative for E, positive for F). Coarse slides apply on every non-first tick; fine slides apply once on tick 0 — the per-step arithmetic is identical: diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index e16e5f0..cc29e79 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -1791,7 +1791,7 @@ function simulateRowState(ptnDat, uptoRow) { let bpm = audio.getBPM(PLAYHEAD) // best-effort starting tempo let speed = audio.getTickRate(PLAYHEAD) let globalVol = 0xFF - let panLaw = 0, toneMode = 0 // toneMode: 0=linear, 1=Amiga, 2=linear-freq, 3=reserved + let toneMode = 0 // 0=linear, 1=Amiga, 2=linear-freq, 3=reserved let memEF = 0, memG = 0 let memHU = { speed: 0, depth: 0 } @@ -1901,9 +1901,7 @@ function simulateRowState(ptnDat, uptoRow) { if (effop !== 0 || effarg !== 0) { if (effop === OP_1) { const flags = (effarg >>> 8) & 0xFF - panLaw = flags & 1 - toneMode = (flags >>> 1) & 3 - // bit 2 reserved (was 'm' fadeout-zero policy; removed) + toneMode = flags & 3 } else if (effop === OP_8) { const x = (effarg >>> 12) & 0xF @@ -2041,7 +2039,7 @@ function simulateRowState(ptnDat, uptoRow) { return { lastNote, lastInst, volAbs, panAbs, pitchOff, bpm, speed, globalVol, - panLaw, toneMode, + toneMode, bitcrushDepth, bitcrushSkip, overdriveAmp, clipMode, glissandoOn, vibratoWave, tremoloWave, panbrelloWave, memEF, memG, memHU, memR, memY, @@ -2242,19 +2240,8 @@ function drawProjectContents(wo) { for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255) let mixerflag = initialTrackerMixerflags - let flagStrSelected = [] - let flagstr = [ - ['Linear pan','EquNrg pan'], - ['Linear pitch','Amiga pitch', 'Linear freq', ''], // TODO MONOTONE uses linear-freq pitch - ] - for (let i = 0; i < flagstr.length; i++) { - if (i != 1 && 1 != 2) { - let s = flagstr[i][(mixerflag >>> i) & 1 != 0] - flagStrSelected.push(s) - } - } - let toneMode = (mixerflag >>> 1) & 3 - flagStrSelected.splice(1, 0, flagstr[1][toneMode]) + let toneModeStr = ['Linear pitch','Amiga pitch','Linear freq',''][mixerflag & 3] + let flagStrSelected = [toneModeStr] let projMeta = { diff --git a/it2taud.py b/it2taud.py index ae979b9..bb6f518 100644 --- a/it2taud.py +++ b/it2taud.py @@ -1821,10 +1821,12 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list, pat_comp = compress_blob(bytes(pat_bin), "pattern bin") cue_comp = compress_blob(bytes(sheet), "cue sheet") - # flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted). - # bit 2 was the old 'm' fadeout-zero policy flag and is now reserved (always 0); fadeout - # scaling is done per-instrument in this converter — see the fadeout pass-through below. - flags_byte = 0x00 if h.linear_slides else 0x02 + # flags byte: bits 0-1 (ff) = tone mode. ff=1 (Amiga period slides) when IT's + # linear_slides flag is clear; ff=0 otherwise. Pan law is fixed engine-wide to + # the equal-energy — no `p` bit any more. Bit 2 was the old 'm' fadeout-zero + # policy flag and is now reserved (always 0); fadeout scaling is done per-instrument + # in this converter — see the fadeout pass-through below. + flags_byte = 0x00 if h.linear_slides else 0x01 # 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)) mixing_vol_taud = min(0xFF, round(h.mix_vol * 255 / 128)) diff --git a/mod2taud.py b/mod2taud.py index 150bd90..89c1c4e 100644 --- a/mod2taud.py +++ b/mod2taud.py @@ -767,13 +767,13 @@ def assemble_taud(mod: dict) -> bytes: pat_comp = compress_blob(bytes(pat_bin), "pattern bin") cue_comp = compress_blob(bytes(cue_sheet), "cue sheet") - # ProTracker is Amiga-period-based by definition, so we set the f bit so + # ProTracker is Amiga-period-based by definition, so we set ff=1 (bits 0-1) so # the engine applies coarse pitch slides in period space (recovers PT's - # characteristic non-linear pitch character). - # bit 2 reserved (was 'm' fadeout-zero policy; removed). PT has no instrument-level - # fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire - # on sample-end or pattern note-cut instead, which matches PT semantics. - flags_byte = 0x02 + # characteristic non-linear pitch character). Pan law is fixed to the + # equal-energy engine-wide. PT has no instrument-level fadeout, so every Taud + # instrument carries fadeout=0 ("no fade") — notes retire on sample-end or + # pattern note-cut instead, which matches PT semantics. + flags_byte = 0x01 song_table = encode_song_entry( song_offset=song_offset, num_voices=n_channels, diff --git a/mon2taud.py b/mon2taud.py index 1afca19..5fbf473 100644 --- a/mon2taud.py +++ b/mon2taud.py @@ -52,12 +52,12 @@ MON_EFFECT_LETTERS = ['0', '1', '2', '3', '4', 'B', 'D', 'F'] MON_NOTE_C4 = 40 # Global behaviour flags byte (Taud Effect 1 / song-table byte 15): -# bit 0 (p) : pan law — leave 0 (linear) for tracker accuracy -# bits 1-2 (ff): tone mode — 2 = linear-frequency (Hz/tick) +# bits 0-1 (ff): tone mode — 2 = linear-frequency (Hz/tick) # Selecting ff=2 makes the engine interpret 1xx/2xx/3xx slide arguments in # audible Hz at the A4=440 Hz reference, matching Monotone's MT_PLAY.PAS # `Frequency:=Frequency±parm1` arithmetic (see MTSRC/MT_PLAY.PAS:606-630). -GLOBAL_FLAGS_LINEAR_FREQ = 0b100 +# Panning law is fixed to the equal-energy — there is no `p` bit any more. +GLOBAL_FLAGS_LINEAR_FREQ = 0b10 # ── Taud container ─────────────────────────────────────────────────────────── @@ -362,10 +362,10 @@ def assemble_taud(mon: dict) -> bytes: # BPM 150 + ticks=mon_speed → row rate = 60/mon_speed (matches Monotone). bpm_stored = 150 - 24 - # Linear-frequency tone mode (ff=2) so 1xx/2xx/3xx Hz/tick semantics survive verbatim; - # pan law stays 0 (linear), bit 2 stays 0 (reserved). Monotone has no instrument-level - # fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire on - # sample-end or pattern note-cut instead. + # Linear-frequency tone mode (ff=2) so 1xx/2xx/3xx Hz/tick semantics survive verbatim. + # Pan law is fixed engine-wide to the equal-energy (no flag). Monotone has no + # instrument-level fadeout, so every Taud instrument carries fadeout=0 ("no fade") — + # notes retire on sample-end or pattern note-cut instead. flags_byte = GLOBAL_FLAGS_LINEAR_FREQ song_table = encode_song_entry( diff --git a/s3m2taud.py b/s3m2taud.py index aeec460..4f9be13 100644 --- a/s3m2taud.py +++ b/s3m2taud.py @@ -812,11 +812,13 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes: cue_comp = compress_blob(bytes(cue_sheet), "cue sheet") # 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). - # bit 2 reserved (was 'm' fadeout-zero policy; removed). S3M has no instrument-level - # fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire on - # sample-end or pattern note-cut effects (SCx) instead, which matches ST3 semantics. - flags_byte = (0x00 if h.linear_slides else 0x02) + # flags byte: bits 0-1 (ff) = tone mode. ff=1 (Amiga period slides) when S3M's + # linear_slides flag is clear; ff=0 otherwise. Pan law is fixed engine-wide to + # the equal-energy — no `p` bit any more. Bit 2 reserved (was 'm' fadeout-zero + # policy; removed). S3M has no instrument-level fadeout, so every Taud instrument + # carries fadeout=0 ("no fade") — notes retire on sample-end or pattern note-cut + # effects (SCx) instead, which matches ST3 semantics. + flags_byte = (0x00 if h.linear_slides else 0x01) song_table = encode_song_entry( song_offset=song_offset, num_voices=C, diff --git a/terranmon.txt b/terranmon.txt index 39d7cf1..fab6582 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2346,7 +2346,7 @@ TODO: [x] FT2/MOD double effects with 00 as arg (500, 600) missing volume column -> easiest solution: fully implement `L xy00` and `K xy00` and map 5xx to L, 6xx to K (xm2taud, mod2taud), Kxy and Lxy verbatim (s3m2taud.py, it2taud.py). This is justified because the volume effects rely on memory when 00 is given, and said memory effect only get recalled when NoteFx is used. TAUD_NOTE_EFFECTS already has detailed implementation notes. Mark those two commands as implemented sorely for tracker compatibility. Also document then implement `Mxx` (set channel volume, not just a note: 0x00 to 0x3F) `Nxy` (channel volume slide: similar to Dxy, but applies to the current channel's volume, not just a note) `Pxy` (channel panning slide. Similar to Dxx: P0y - to the right, Px0 - to the left, PFy - fine pan right, PxF - fine pan left) effects [x] 8 MB sample RAM via 512k banks - [ ] remove panning mode selection and replace global panning rule to 3 dB rule (not the equal energy) + [x] remove panning mode selection and replace global panning rule to equal energy, also move the 'ff' flags to bit 0..1 [ ] low-number voleffs are too quiet (needs elaboration and test cases) @@ -2449,12 +2449,12 @@ Play Head Flags Byte 2 - PCM Mode: Write non-zero value to start uploading; always 0 when read - Tracker Mode: Global mixer flags. Maps directly to Taud effect symbol '1' - 0b 0000 0ffp - p: panning mode (0: linear, 1: equal-power) + 0b 0000 00ff ff: pitchshift mode (0: linear pitch slides, 1: Amiga period slides, 2: linear-frequency slides, 3: reserved) Tracker command may change the mixer state, but the changes WILL NOT BE REFLECTED BACK. Starting a new song will use whatever written to this register. In other words, changes made by songs will not persist. + Panning law is fixed to the equal-energy; there is no runtime selection. Byte 3 (Tracker Mode) - BPM (24 to 279. Play Data will change this register) Byte 4 (Tracker Mode) @@ -2573,12 +2573,8 @@ 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 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') - 0b 0000 0ffp - p: panning law (0: linear, 1: equal-power) + 0b 0000 00ff ff: tone mode (0: linear pitch slides, 1: Amiga period slides, 2: linear-frequency slides, 3: reserved) - (bit 2 reserved — was 'm' fadeout-zero policy, removed; fadeout - scaling now lives entirely in the converter — see byte 172/173 - of the instrument record for engine semantics) Uint8 Song global volume * ImpulseTracker has range of 0..128; multiply by (255/128) then round to int Uint8 Song mixing volume diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index c3651e2..1da3b51 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -135,10 +135,10 @@ class AudioJSR223Delegate(private val vm: VM) { getFirstSnd()?.playheads?.get(playhead)?.let { ph -> ph.initialGlobalFlags = flags ph.trackerState?.let { ts -> - ts.panLaw = flags and 1 - ts.toneMode = (flags ushr 1) and 3 - // bit 2 reserved (was 'm' fadeout-zero policy; removed — see AudioAdapter.kt - // and TAUD_NOTE_EFFECTS.md §1 "Volume Fadeout") + ts.toneMode = flags and 3 + // Bits 2-7 reserved. Bit 2 was the old 'm' fadeout-zero policy; removed. + // Pan law is fixed to the equal-energy engine-wide — no flag bit any more. + // See AudioAdapter.kt and TAUD_NOTE_EFFECTS.md §1. } } } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 43f157a..0271e9c 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -12,12 +12,12 @@ import net.torvald.tsvm.ThreeFiveMiniUfloat import net.torvald.tsvm.VM import net.torvald.tsvm.toInt import java.io.ByteArrayInputStream -import kotlin.math.cos import kotlin.math.log2 import kotlin.math.pow import kotlin.math.roundToInt -import kotlin.math.sin import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable { private fun printdbg(msg: Any) { @@ -2099,12 +2099,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { EffectOp.OP_NONE -> {} EffectOp.OP_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 - // bits 1-2 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick), + // bits 0-1 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick), // 3=reserved + // Panning law is fixed to the equal-energy; no runtime selection. val flags = rawArg ushr 8 - ts.panLaw = flags and 1 - ts.toneMode = (flags ushr 1) and 3 + ts.toneMode = flags and 3 } EffectOp.OP_8 -> { // 8 $xyzz — Bitcrusher. See TAUD_NOTE_EFFECTS.md §8. @@ -2812,18 +2811,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { val envPanRaw = (voice.envPan * 255.0).roundToInt().coerceIn(0, 255) (voice.channelPan + envPanRaw - 128 + voice.randomPanBias).coerceIn(0, 255) } else (voice.channelPan + voice.randomPanBias).coerceIn(0, 255) - val lGain: Double - val rGain: Double - when (ts.panLaw) { - 1 -> { // equal-power: constant loudness at centre (0.707 each) - lGain = cos(PI * pan / 512.0) - rGain = sin(PI * pan / 512.0) - } - else -> { // linear balance (tracker default): centre gives 0 dB on both channels - lGain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0 - rGain = if (pan < 0x80) pan / 128.0 else 1.0 - } - } + // equal-energy pan law + val lGain = cos(PI * pan / 512.0) + val rGain = sin(PI * pan / 512.0) // Sample-end ramp-out: snapshot gain, advance the ramp, deactivate at zero. val rampGain = if (voice.rampOutSamples > 0) { val g = voice.rampOutGain @@ -2850,18 +2840,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255) (bg.channelPan + envPanRaw - 128 + bg.randomPanBias).coerceIn(0, 255) } else (bg.channelPan + bg.randomPanBias).coerceIn(0, 255) - val lGain: Double - val rGain: Double - when (ts.panLaw) { - 1 -> { - lGain = cos(PI * pan / 512.0) - rGain = sin(PI * pan / 512.0) - } - else -> { - lGain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0 - rGain = if (pan < 0x80) pan / 128.0 else 1.0 - } - } + val lGain = cos(PI * pan / 512.0) + val rGain = sin(PI * pan / 512.0) val rampGain = if (bg.rampOutSamples > 0) { val g = bg.rampOutGain bg.rampOutGain -= bg.rampOutStep @@ -3251,8 +3231,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { var firstRow = true val voices = Array(20) { Voice() } - // Global mixer config (effect 1). - var panLaw = 0 // 0 = linear balance (default), 1 = equal-power + // Global mixer config (effect 1). Panning law is fixed to the equal-energy. // Tone-slide mode for E / F / G effects (terranmon.txt §Song Table flags byte): // 0 = linear pitch slides (4096-TET units, default) // 1 = Amiga period slides (raw PT period units, applied in period space) @@ -3370,8 +3349,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { 7 -> if (isPcmMode) { pcmUpload = true } else { initialGlobalFlags = byte trackerState?.let { ts -> - ts.panLaw = byte and 1 - ts.toneMode = (byte ushr 1) and 3 + ts.toneMode = byte and 3 } } 8 -> { bpm = byte + 24 } @@ -3405,8 +3383,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { ts.patternDelayRemaining = 0; ts.patternDelayActive = false ts.sexWinningChannel = -1 ts.finePatternDelayExtra = 0 - ts.panLaw = initialGlobalFlags and 1 - ts.toneMode = (initialGlobalFlags ushr 1) and 3 + ts.toneMode = initialGlobalFlags and 3 ts.voices.forEach { it.active = false it.channelVolume = 0x3F diff --git a/xm2taud.py b/xm2taud.py index ac8f759..cb8adf9 100644 --- a/xm2taud.py +++ b/xm2taud.py @@ -1425,11 +1425,13 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes: cue_comp = compress_blob(bytes(sheet), "cue sheet") # Flags byte: - # bit 1 (f) = Amiga pitch-slide mode (set when XM uses Amiga period table). - # bit 2 = reserved (was 'm' fadeout-zero policy; removed). XM fadeout values are - # now scaled per-instrument above (÷32 with round-to-nearest), so the - # engine sees Taud-native units and uses its single divisor of 1024. - flags_byte = (0x00 if h.linear_freq else 0x02) + # bits 0-1 (ff) = tone mode. ff=1 (Amiga period slides) when XM uses the Amiga + # period table; ff=0 otherwise. Pan law is fixed engine-wide to + # the equal-energy — no `p` bit any more. + # bit 2 = reserved (was 'm' fadeout-zero policy; removed). XM fadeout values + # are now scaled per-instrument above (÷32 with round-to-nearest), so + # the engine sees Taud-native units and uses its single divisor of 1024. + flags_byte = (0x00 if h.linear_freq else 0x01) song_table = encode_song_entry( song_offset=song_offset, num_voices=C,