BPM is now 25..280

This commit is contained in:
minjaesong
2026-05-08 20:40:25 +09:00
parent ed3bbb6ffe
commit 8e6f597e9b
9 changed files with 31 additions and 31 deletions

View File

@@ -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

View File

@@ -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 ──────────────────────────────────────────────────────────

View File

@@ -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

View File

@@ -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") —

View File

@@ -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)

View File

@@ -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

View File

@@ -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 }

View File

@@ -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)
}*/

View File

@@ -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 ─────────────────────────────────────────────────