Taud: equal-energy panning only

This commit is contained in:
minjaesong
2026-05-08 20:23:46 +09:00
parent 27b0f2e63f
commit ed3bbb6ffe
10 changed files with 63 additions and 103 deletions

View File

@@ -181,7 +181,7 @@ Coarse and fine modes are distinguished by the high nibble of the argument:
- **MONOTONE source** (Taud `ff = 2`): - **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. - 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. 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: **Plain.** Sets mixer-wide behaviour flags. Available flags are:
0b 0000 0ffp 0b 0000 00ff
- 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.
- 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 = 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 = 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). - 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 ### 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. - **MOD / S3M / MON**: source has no instrument-level fadeout. Converter writes Taud `0`. Notes retire on sample-end or pattern note-cut.
**Implementation.** **Implementation.**
- Panning-linear: - Panning (equal-energy):
- 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:
- L_gain = cos(πx / 512.0) - L_gain = cos(πx / 512.0)
- R_gain = sin(π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: - 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:

View File

@@ -1791,7 +1791,7 @@ function simulateRowState(ptnDat, uptoRow) {
let bpm = audio.getBPM(PLAYHEAD) // best-effort starting tempo let bpm = audio.getBPM(PLAYHEAD) // best-effort starting tempo
let speed = audio.getTickRate(PLAYHEAD) let speed = audio.getTickRate(PLAYHEAD)
let globalVol = 0xFF 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 memEF = 0, memG = 0
let memHU = { speed: 0, depth: 0 } let memHU = { speed: 0, depth: 0 }
@@ -1901,9 +1901,7 @@ function simulateRowState(ptnDat, uptoRow) {
if (effop !== 0 || effarg !== 0) { if (effop !== 0 || effarg !== 0) {
if (effop === OP_1) { if (effop === OP_1) {
const flags = (effarg >>> 8) & 0xFF const flags = (effarg >>> 8) & 0xFF
panLaw = flags & 1 toneMode = flags & 3
toneMode = (flags >>> 1) & 3
// bit 2 reserved (was 'm' fadeout-zero policy; removed)
} }
else if (effop === OP_8) { else if (effop === OP_8) {
const x = (effarg >>> 12) & 0xF const x = (effarg >>> 12) & 0xF
@@ -2041,7 +2039,7 @@ function simulateRowState(ptnDat, uptoRow) {
return { lastNote, lastInst, volAbs, panAbs, pitchOff, return { lastNote, lastInst, volAbs, panAbs, pitchOff,
bpm, speed, globalVol, bpm, speed, globalVol,
panLaw, toneMode, toneMode,
bitcrushDepth, bitcrushSkip, overdriveAmp, clipMode, bitcrushDepth, bitcrushSkip, overdriveAmp, clipMode,
glissandoOn, vibratoWave, tremoloWave, panbrelloWave, glissandoOn, vibratoWave, tremoloWave, panbrelloWave,
memEF, memG, memHU, memR, memY, 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) for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
let mixerflag = initialTrackerMixerflags let mixerflag = initialTrackerMixerflags
let flagStrSelected = [] let toneModeStr = ['Linear pitch','Amiga pitch','Linear freq',''][mixerflag & 3]
let flagstr = [ let flagStrSelected = [toneModeStr]
['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 projMeta = { let projMeta = {

View File

@@ -1821,10 +1821,12 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
pat_comp = compress_blob(bytes(pat_bin), "pattern bin") pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = compress_blob(bytes(sheet), "cue sheet") cue_comp = compress_blob(bytes(sheet), "cue sheet")
# flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted). # flags byte: bits 0-1 (ff) = tone mode. ff=1 (Amiga period slides) when IT's
# bit 2 was the old 'm' fadeout-zero policy flag and is now reserved (always 0); fadeout # linear_slides flag is clear; ff=0 otherwise. Pan law is fixed engine-wide to
# scaling is done per-instrument in this converter — see the fadeout pass-through below. # the equal-energy — no `p` bit any more. Bit 2 was the old 'm' fadeout-zero
flags_byte = 0x00 if h.linear_slides else 0x02 # 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). # 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))
mixing_vol_taud = min(0xFF, round(h.mix_vol * 255 / 128)) mixing_vol_taud = min(0xFF, round(h.mix_vol * 255 / 128))

View File

@@ -767,13 +767,13 @@ def assemble_taud(mod: dict) -> bytes:
pat_comp = compress_blob(bytes(pat_bin), "pattern bin") pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = compress_blob(bytes(cue_sheet), "cue sheet") 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 # the engine applies coarse pitch slides in period space (recovers PT's
# characteristic non-linear pitch character). # characteristic non-linear pitch character). Pan law is fixed to the
# bit 2 reserved (was 'm' fadeout-zero policy; removed). PT has no instrument-level # equal-energy engine-wide. PT has no instrument-level fadeout, so every Taud
# fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire # instrument carries fadeout=0 ("no fade") — notes retire on sample-end or
# on sample-end or pattern note-cut instead, which matches PT semantics. # pattern note-cut instead, which matches PT semantics.
flags_byte = 0x02 flags_byte = 0x01
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,

View File

@@ -52,12 +52,12 @@ MON_EFFECT_LETTERS = ['0', '1', '2', '3', '4', 'B', 'D', 'F']
MON_NOTE_C4 = 40 MON_NOTE_C4 = 40
# Global behaviour flags byte (Taud Effect 1 / song-table byte 15): # Global behaviour flags byte (Taud Effect 1 / song-table byte 15):
# bit 0 (p) : pan law — leave 0 (linear) for tracker accuracy # bits 0-1 (ff): tone mode — 2 = linear-frequency (Hz/tick)
# bits 1-2 (ff): tone mode — 2 = linear-frequency (Hz/tick)
# Selecting ff=2 makes the engine interpret 1xx/2xx/3xx slide arguments in # 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 # 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). # `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 ─────────────────────────────────────────────────────────── # ── 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 150 + ticks=mon_speed → row rate = 60/mon_speed (matches Monotone).
bpm_stored = 150 - 24 bpm_stored = 150 - 24
# Linear-frequency tone mode (ff=2) so 1xx/2xx/3xx Hz/tick semantics survive verbatim; # 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 # Pan law is fixed engine-wide to the equal-energy (no flag). Monotone has no
# fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire on # instrument-level fadeout, so every Taud instrument carries fadeout=0 ("no fade") —
# sample-end or pattern note-cut instead. # notes retire on sample-end or pattern note-cut instead.
flags_byte = GLOBAL_FLAGS_LINEAR_FREQ flags_byte = GLOBAL_FLAGS_LINEAR_FREQ
song_table = encode_song_entry( song_table = encode_song_entry(

View File

@@ -812,11 +812,13 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
cue_comp = compress_blob(bytes(cue_sheet), "cue sheet") cue_comp = compress_blob(bytes(cue_sheet), "cue sheet")
# 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: bits 0-1 (ff) = tone mode. ff=1 (Amiga period slides) when S3M's
# bit 2 reserved (was 'm' fadeout-zero policy; removed). S3M has no instrument-level # linear_slides flag is clear; ff=0 otherwise. Pan law is fixed engine-wide to
# fadeout, so every Taud instrument carries fadeout=0 ("no fade") — notes retire on # the equal-energy — no `p` bit any more. Bit 2 reserved (was 'm' fadeout-zero
# sample-end or pattern note-cut effects (SCx) instead, which matches ST3 semantics. # policy; removed). S3M has no instrument-level fadeout, so every Taud instrument
flags_byte = (0x00 if h.linear_slides else 0x02) # 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_table = encode_song_entry(
song_offset=song_offset, song_offset=song_offset,
num_voices=C, num_voices=C,

View File

@@ -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. [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 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 [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) [ ] low-number voleffs are too quiet (needs elaboration and test cases)
@@ -2449,12 +2449,12 @@ Play Head Flags
Byte 2 Byte 2
- PCM Mode: Write non-zero value to start uploading; always 0 when read - 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' - Tracker Mode: Global mixer flags. Maps directly to Taud effect symbol '1'
0b 0000 0ffp 0b 0000 00ff
p: panning mode (0: linear, 1: equal-power)
ff: pitchshift mode (0: linear pitch slides, 1: Amiga period slides, 2: linear-frequency slides, 3: reserved) 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. 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 Starting a new song will use whatever written to this register. In other words, changes
made by songs will not persist. made by songs will not persist.
Panning law is fixed to the equal-energy; there is no runtime selection.
Byte 3 (Tracker Mode) Byte 3 (Tracker Mode)
- BPM (24 to 279. Play Data will change this register) - BPM (24 to 279. Play Data will change this register)
Byte 4 (Tracker Mode) 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 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 0ffp 0b 0000 00ff
p: panning law (0: linear, 1: equal-power)
ff: tone mode (0: linear pitch slides, 1: Amiga period slides, 2: linear-frequency slides, 3: reserved) 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 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

@@ -135,10 +135,10 @@ class AudioJSR223Delegate(private val vm: VM) {
getFirstSnd()?.playheads?.get(playhead)?.let { ph -> getFirstSnd()?.playheads?.get(playhead)?.let { ph ->
ph.initialGlobalFlags = flags ph.initialGlobalFlags = flags
ph.trackerState?.let { ts -> ph.trackerState?.let { ts ->
ts.panLaw = flags and 1 ts.toneMode = flags and 3
ts.toneMode = (flags ushr 1) and 3 // Bits 2-7 reserved. Bit 2 was the old 'm' fadeout-zero policy; removed.
// bit 2 reserved (was 'm' fadeout-zero policy; removed — see AudioAdapter.kt // Pan law is fixed to the equal-energy engine-wide — no flag bit any more.
// and TAUD_NOTE_EFFECTS.md §1 "Volume Fadeout") // See AudioAdapter.kt and TAUD_NOTE_EFFECTS.md §1.
} }
} }
} }

View File

@@ -12,12 +12,12 @@ import net.torvald.tsvm.ThreeFiveMiniUfloat
import net.torvald.tsvm.VM import net.torvald.tsvm.VM
import net.torvald.tsvm.toInt import net.torvald.tsvm.toInt
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import kotlin.math.cos
import kotlin.math.log2 import kotlin.math.log2
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable { private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
private fun printdbg(msg: Any) { private fun printdbg(msg: Any) {
@@ -2099,12 +2099,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
EffectOp.OP_NONE -> {} EffectOp.OP_NONE -> {}
EffectOp.OP_1 -> { EffectOp.OP_1 -> {
// 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 // bits 0-1 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick),
// bits 1-2 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick),
// 3=reserved // 3=reserved
// Panning law is fixed to the equal-energy; no runtime selection.
val flags = rawArg ushr 8 val flags = rawArg ushr 8
ts.panLaw = flags and 1 ts.toneMode = flags and 3
ts.toneMode = (flags ushr 1) and 3
} }
EffectOp.OP_8 -> { EffectOp.OP_8 -> {
// 8 $xyzz — Bitcrusher. See TAUD_NOTE_EFFECTS.md §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) val envPanRaw = (voice.envPan * 255.0).roundToInt().coerceIn(0, 255)
(voice.channelPan + envPanRaw - 128 + voice.randomPanBias).coerceIn(0, 255) (voice.channelPan + envPanRaw - 128 + voice.randomPanBias).coerceIn(0, 255)
} else (voice.channelPan + voice.randomPanBias).coerceIn(0, 255) } else (voice.channelPan + voice.randomPanBias).coerceIn(0, 255)
val lGain: Double // equal-energy pan law
val rGain: Double val lGain = cos(PI * pan / 512.0)
when (ts.panLaw) { val rGain = sin(PI * pan / 512.0)
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
}
}
// Sample-end ramp-out: snapshot gain, advance the ramp, deactivate at zero. // Sample-end ramp-out: snapshot gain, advance the ramp, deactivate at zero.
val rampGain = if (voice.rampOutSamples > 0) { val rampGain = if (voice.rampOutSamples > 0) {
val g = voice.rampOutGain 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) val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255)
(bg.channelPan + envPanRaw - 128 + bg.randomPanBias).coerceIn(0, 255) (bg.channelPan + envPanRaw - 128 + bg.randomPanBias).coerceIn(0, 255)
} else (bg.channelPan + bg.randomPanBias).coerceIn(0, 255) } else (bg.channelPan + bg.randomPanBias).coerceIn(0, 255)
val lGain: Double val lGain = cos(PI * pan / 512.0)
val rGain: Double val rGain = sin(PI * pan / 512.0)
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 rampGain = if (bg.rampOutSamples > 0) { val rampGain = if (bg.rampOutSamples > 0) {
val g = bg.rampOutGain val g = bg.rampOutGain
bg.rampOutGain -= bg.rampOutStep bg.rampOutGain -= bg.rampOutStep
@@ -3251,8 +3231,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var firstRow = true var firstRow = true
val voices = Array(20) { Voice() } val voices = Array(20) { Voice() }
// Global mixer config (effect 1). // Global mixer config (effect 1). Panning law is fixed to the equal-energy.
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
// Tone-slide mode for E / F / G effects (terranmon.txt §Song Table flags byte): // Tone-slide mode for E / F / G effects (terranmon.txt §Song Table flags byte):
// 0 = linear pitch slides (4096-TET units, default) // 0 = linear pitch slides (4096-TET units, default)
// 1 = Amiga period slides (raw PT period units, applied in period space) // 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 { 7 -> if (isPcmMode) { pcmUpload = true } else {
initialGlobalFlags = byte initialGlobalFlags = byte
trackerState?.let { ts -> trackerState?.let { ts ->
ts.panLaw = byte and 1 ts.toneMode = byte and 3
ts.toneMode = (byte ushr 1) and 3
} }
} }
8 -> { bpm = byte + 24 } 8 -> { bpm = byte + 24 }
@@ -3405,8 +3383,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
ts.patternDelayRemaining = 0; ts.patternDelayActive = false ts.patternDelayRemaining = 0; ts.patternDelayActive = false
ts.sexWinningChannel = -1 ts.sexWinningChannel = -1
ts.finePatternDelayExtra = 0 ts.finePatternDelayExtra = 0
ts.panLaw = initialGlobalFlags and 1 ts.toneMode = initialGlobalFlags and 3
ts.toneMode = (initialGlobalFlags ushr 1) and 3
ts.voices.forEach { ts.voices.forEach {
it.active = false it.active = false
it.channelVolume = 0x3F it.channelVolume = 0x3F

View File

@@ -1425,11 +1425,13 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes:
cue_comp = compress_blob(bytes(sheet), "cue sheet") cue_comp = compress_blob(bytes(sheet), "cue sheet")
# Flags byte: # Flags byte:
# bit 1 (f) = Amiga pitch-slide mode (set when XM uses Amiga period table). # bits 0-1 (ff) = tone mode. ff=1 (Amiga period slides) when XM uses the Amiga
# bit 2 = reserved (was 'm' fadeout-zero policy; removed). XM fadeout values are # period table; ff=0 otherwise. Pan law is fixed engine-wide to
# now scaled per-instrument above (÷32 with round-to-nearest), so the # the equal-energy — no `p` bit any more.
# engine sees Taud-native units and uses its single divisor of 1024. # bit 2 = reserved (was 'm' fadeout-zero policy; removed). XM fadeout values
flags_byte = (0x00 if h.linear_freq else 0x02) # 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_table = encode_song_entry(
song_offset=song_offset, song_offset=song_offset,
num_voices=C, num_voices=C,