diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index cc29e79..9f5db96 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -534,7 +534,7 @@ function loadTaud(filePath, songIndex) { return { filePath, version, numSongs, numVoices, numPats, - bpm: (bpmStored + 24) & 0xFF, tickRate, + bpm: bpmStored + 25, tickRate, patterns, cues, lastActiveCue } } @@ -2005,7 +2005,7 @@ function simulateRowState(ptnDat, uptoRow) { else if (effop === OP_T) { const hi = (effarg >>> 8) & 0xFF if (hi !== 0) { - bpm = Math.max(24, Math.min(280, hi + 0x18)) + bpm = Math.max(25, Math.min(280, hi + 0x19)) } else { const low = effarg & 0xFF if ((low & 0xF0) === 0x00 || (low & 0xF0) === 0x10) memTSlide = low diff --git a/it2taud.py b/it2taud.py index bb6f518..c355d29 100644 --- a/it2taud.py +++ b/it2taud.py @@ -906,7 +906,7 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0, if cmd == EFF_T: if arg >= 0x20: - return (TOP_T, ((arg - 0x18) & 0xFF) << 8, None, None) + return (TOP_T, ((arg - 0x19) & 0xFF) << 8, None, None) return (TOP_T, arg & 0xFF, None, None) if cmd == EFF_V: @@ -1734,8 +1734,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list, # ── BPM / speed ────────────────────────────────────────────────────────── speed, tempo = find_initial_bpm_speed(patterns_rows, h.order_list, h.initial_speed, h.initial_tempo) - tempo = max(24, min(280, tempo)) - bpm_stored = (tempo - 24) & 0xFF + tempo = max(25, min(280, tempo)) + bpm_stored = (tempo - 25) & 0xFF vprint(f" initial speed={speed}, tempo={tempo} BPM") # ── Pattern bin ────────────────────────────────────────────────────────── diff --git a/mod2taud.py b/mod2taud.py index 89c1c4e..1b549c9 100644 --- a/mod2taud.py +++ b/mod2taud.py @@ -367,7 +367,7 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple: if arg == 0: return (TOP_NONE, 0, None, None) return (TOP_A, (arg & 0xFF) << 8, None, None) - return (TOP_T, ((arg - 0x18) & 0xFF) << 8, None, None) + return (TOP_T, ((arg - 0x19) & 0xFF) << 8, None, None) return (TOP_NONE, 0, None, None) @@ -721,8 +721,8 @@ def assemble_taud(mod: dict) -> bytes: comp_size = len(compressed) speed, tempo = find_initial_bpm_speed(patterns, order_list) - tempo = max(24, min(280, tempo)) - bpm_stored = (tempo - 24) & 0xFF + tempo = max(25, min(280, tempo)) + bpm_stored = (tempo - 25) & 0xFF vprint(f" initial speed={speed}, tempo(BPM)={tempo}") song_offset = TAUD_HEADER_SIZE + comp_size + TAUD_SONG_ENTRY diff --git a/mon2taud.py b/mon2taud.py index 5fbf473..8590d49 100644 --- a/mon2taud.py +++ b/mon2taud.py @@ -361,7 +361,7 @@ def assemble_taud(mon: dict) -> bytes: song_offset = TAUD_HEADER_SIZE + comp_size + TAUD_SONG_ENTRY # BPM 150 + ticks=mon_speed → row rate = 60/mon_speed (matches Monotone). - bpm_stored = 150 - 24 + bpm_stored = 150 - 25 # 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") — diff --git a/s3m2taud.py b/s3m2taud.py index 4f9be13..65341b6 100644 --- a/s3m2taud.py +++ b/s3m2taud.py @@ -351,7 +351,7 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0, if cmd == EFF_T: if arg >= 0x20: - return (TOP_T, ((arg - 0x18) & 0xFF) << 8, None, None) + return (TOP_T, ((arg - 0x19) & 0xFF) << 8, None, None) # OpenMPT slide forms: $0y down per tick, $1y up per tick. return (TOP_T, arg & 0xFF, None, None) @@ -757,8 +757,8 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes: # Initial BPM / speed speed, tempo = find_initial_bpm_speed(patterns, h.order_list, h.initial_speed, h.initial_tempo) - tempo = max(24, min(280, tempo)) - bpm_stored = (tempo - 24) & 0xFF + tempo = max(25, min(280, tempo)) + bpm_stored = (tempo - 25) & 0xFF vprint(f" initial speed={speed}, tempo(BPM)={tempo}") # Song offset = header(32) + compressed + song_table(8) diff --git a/terranmon.txt b/terranmon.txt index fab6582..013decb 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2456,7 +2456,7 @@ Play Head Flags 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) + - BPM (25 to 280. Play Data will change this register) Byte 4 (Tracker Mode) - Tick Rate (Play Data will change this register) @@ -2486,8 +2486,8 @@ Play Head Flags Table of 3.5 Minifloat values (CSV). Rebiased 2026-05-07 so the smallest non-zero step is 1/256 s and the maximum is 15.75 s — every cell is the original LUT value divided by 8. Chosen for -tracker envelopes: a single song tick (≈ 8.9 ms at BPM 280, ≈ 41.7 ms at -BPM 24) now lands within ±17 % of an LUT entry across the whole supported +tracker envelopes: a single song tick (≈ 8.9 ms at BPM 280, ≈ 100 ms at +BPM 25) now lands within ±17 % of an LUT entry across the whole supported BPM range; the previous bias was ±150 % at common BPMs. ,000,001,010,011,100,101,110,111,MSB 00000,0,0.125,0.25,0.5,1,2,4,8 @@ -2568,7 +2568,7 @@ Endianness: Little Uint32 Song offset Uint8 Number of voices Uint16 Number of patterns (0 is invalid. pattern bin length = numPats * 8 bytes) - Uint8 Initial BPM (bias of -24. 0x00=24, 0xFF=279) + Uint8 Initial BPM (bias of -25. 0x00=25, 0xFF=280) Uint8 Initial Tickrate (0 is invalid) 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 @@ -2738,7 +2738,7 @@ The halt instruction (byte value 0x01 at cue offset 30) is placed on the last ac ## Tempo mapping -S3M BPM is stored as a raw decimal value. Taud's initial BPM byte uses a bias of -24 (byte 0x00 = 24 BPM, 0xFF = 279 BPM). Conversion: taud_byte = bpm - 24. The converter also scans row 0 of the first pattern in the order list for A (set speed) and T (set tempo) effects and uses those values in preference to the S3M header defaults. +S3M BPM is stored as a raw decimal value. Taud's initial BPM byte uses a bias of -25 (byte 0x00 = 25 BPM, 0xFF = 280 BPM). Conversion: taud_byte = bpm - 25. The converter also scans row 0 of the first pattern in the order list for A (set speed) and T (set tempo) effects and uses those values in preference to the S3M header defaults. ## Global volume diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 1da3b51..9583a67 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -75,7 +75,7 @@ class AudioJSR223Delegate(private val vm: VM) { fun startSampleUpload(playhead: Int) { getPlayhead(playhead)?.pcmUpload = true } - fun setBPM(playhead: Int, bpm: Int) { getPlayhead(playhead)?.bpm = (bpm - 24).and(255) + 24 } + fun setBPM(playhead: Int, bpm: Int) { getPlayhead(playhead)?.bpm = (bpm - 25).and(255) + 25 } fun getBPM(playhead: Int) = getPlayhead(playhead)?.bpm fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 0271e9c..745abd7 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -2343,7 +2343,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { val hi = (rawArg ushr 8) and 0xFF if (hi != 0) { val tempoByte = hi - playhead.bpm = (tempoByte + 0x18).coerceIn(24, 280) + playhead.bpm = (tempoByte + 0x19).coerceIn(25, 280) } else { val low = rawArg and 0xFF when (low and 0xF0) { @@ -2672,8 +2672,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // Tempo slide — applied once per tick at the playhead level (any channel that armed it). for (voice in ts.voices) { if (voice.tempoSlideDir != 0 && ts.tickInRow > 0) { - val tempoByte = (playhead.bpm - 0x18 + voice.tempoSlideDir * voice.tempoSlideAmount).coerceIn(0, 0xFF) - playhead.bpm = (tempoByte + 0x18).coerceIn(24, 280) + val tempoByte = (playhead.bpm - 0x19 + voice.tempoSlideDir * voice.tempoSlideAmount).coerceIn(0, 0xFF) + playhead.bpm = (tempoByte + 0x19).coerceIn(25, 280) } } @@ -3274,7 +3274,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { var masterVolume: Int = 0, var masterPan: Int = 128, // var samplingRateMult: ThreeFiveMiniUfloat = ThreeFiveMiniUfloat(32), - var bpm: Int = 125, // BPM, derived from tempoByte + 24. Spec default $65 ⇒ 125 BPM. + var bpm: Int = 125, // BPM, derived from tempoByte + 25. Spec default $64 ⇒ 125 BPM. var tickRate: Int = 6, var pcmUpload: Boolean = false, var patBank1: Int = 0, @@ -3322,7 +3322,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { 5 -> masterPan.toByte() 6 -> (isPcmMode.toInt(7) or isPlaying.toInt(4) or pcmQueueSizeIndex.and(15)).toByte() 7 -> initialGlobalFlags.toByte() - 8 -> (bpm - 24).toByte() + 8 -> (bpm - 25).toByte() 9 -> tickRate.toByte() else -> throw InternalError("Bad offset $index") } @@ -3352,16 +3352,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { ts.toneMode = byte and 3 } } - 8 -> { bpm = byte + 24 } + 8 -> { bpm = byte + 25 } 9 -> { tickRate = byte } else -> throw InternalError("Bad offset $index") } } - /*fun getSamplingRate() = 30000 - ((bpm - 24).and(255) or tickRate.and(255).shl(8)).toShort().toInt() + /*fun getSamplingRate() = 30000 - ((bpm - 25).and(255) or tickRate.and(255).shl(8)).toShort().toInt() fun setSamplingRate(rate: Int) { val rateDiff = (rate.coerceIn(0, 95535) - 30000).toShort().toInt() - bpm = rateDiff.and(255) + 24 + bpm = rateDiff.and(255) + 25 tickRate = rateDiff.ushr(8).and(255) }*/ diff --git a/xm2taud.py b/xm2taud.py index cb8adf9..56e34b7 100644 --- a/xm2taud.py +++ b/xm2taud.py @@ -598,8 +598,8 @@ def encode_effect_xm(cmd: int, arg: int, ch: int = 0, row: int = 0, return (TOP_NONE, 0, None, None) if arg < 0x20: return (TOP_A, (arg & 0xFF) << 8, None, None) - # Tempo: Taud T uses bias of -24 in stored form; mirror it2taud: - return (TOP_T, ((arg - 0x18) & 0xFF) << 8, None, None) + # Tempo: Taud T uses bias of -25 in stored form; mirror it2taud: + return (TOP_T, ((arg - 0x19) & 0xFF) << 8, None, None) if cmd == 0x10: # Set global volume 0..64 → Taud V (×4 to fit 0..255). @@ -1249,7 +1249,7 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes: # XM envelope frames advance once per row tick. Tick rate is derived # from BPM the same way ProTracker derives it: ticks_per_sec = BPM × 2/5 # (matches MilkyTracker's tick clock and it2taud's ticks_per_sec). - tempo_for_envs = max(24, min(280, h.default_bpm if h.default_bpm > 0 else 125)) + tempo_for_envs = max(25, min(280, h.default_bpm if h.default_bpm > 0 else 125)) ticks_per_sec = max(1.0, tempo_for_envs * 2.0 / 5.0) # ── Build XM-instrument → list of Taud slot proxies ───────────────────── @@ -1302,8 +1302,8 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes: # ── Tempo / speed ─────────────────────────────────────────────────────── speed = h.default_speed if h.default_speed > 0 else 6 tempo = h.default_bpm if h.default_bpm > 0 else 125 - tempo = max(24, min(280, tempo)) - bpm_stored = (tempo - 24) & 0xFF + tempo = max(25, min(280, tempo)) + bpm_stored = (tempo - 25) & 0xFF vprint(f" initial speed={speed}, tempo={tempo} BPM") # ── Channels / cue list ─────────────────────────────────────────────────