mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
BPM is now 25..280
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") —
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}*/
|
||||
|
||||
|
||||
10
xm2taud.py
10
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 ─────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user