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 {
|
return {
|
||||||
filePath, version, numSongs, numVoices, numPats,
|
filePath, version, numSongs, numVoices, numPats,
|
||||||
bpm: (bpmStored + 24) & 0xFF, tickRate,
|
bpm: bpmStored + 25, tickRate,
|
||||||
patterns, cues, lastActiveCue
|
patterns, cues, lastActiveCue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2005,7 +2005,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
else if (effop === OP_T) {
|
else if (effop === OP_T) {
|
||||||
const hi = (effarg >>> 8) & 0xFF
|
const hi = (effarg >>> 8) & 0xFF
|
||||||
if (hi !== 0) {
|
if (hi !== 0) {
|
||||||
bpm = Math.max(24, Math.min(280, hi + 0x18))
|
bpm = Math.max(25, Math.min(280, hi + 0x19))
|
||||||
} else {
|
} else {
|
||||||
const low = effarg & 0xFF
|
const low = effarg & 0xFF
|
||||||
if ((low & 0xF0) === 0x00 || (low & 0xF0) === 0x10) memTSlide = low
|
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 cmd == EFF_T:
|
||||||
if arg >= 0x20:
|
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)
|
return (TOP_T, arg & 0xFF, None, None)
|
||||||
|
|
||||||
if cmd == EFF_V:
|
if cmd == EFF_V:
|
||||||
@@ -1734,8 +1734,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
# ── BPM / speed ──────────────────────────────────────────────────────────
|
# ── BPM / speed ──────────────────────────────────────────────────────────
|
||||||
speed, tempo = find_initial_bpm_speed(patterns_rows, h.order_list,
|
speed, tempo = find_initial_bpm_speed(patterns_rows, h.order_list,
|
||||||
h.initial_speed, h.initial_tempo)
|
h.initial_speed, h.initial_tempo)
|
||||||
tempo = max(24, min(280, tempo))
|
tempo = max(25, min(280, tempo))
|
||||||
bpm_stored = (tempo - 24) & 0xFF
|
bpm_stored = (tempo - 25) & 0xFF
|
||||||
vprint(f" initial speed={speed}, tempo={tempo} BPM")
|
vprint(f" initial speed={speed}, tempo={tempo} BPM")
|
||||||
|
|
||||||
# ── Pattern bin ──────────────────────────────────────────────────────────
|
# ── Pattern bin ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple:
|
|||||||
if arg == 0:
|
if arg == 0:
|
||||||
return (TOP_NONE, 0, None, None)
|
return (TOP_NONE, 0, None, None)
|
||||||
return (TOP_A, (arg & 0xFF) << 8, 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)
|
return (TOP_NONE, 0, None, None)
|
||||||
|
|
||||||
@@ -721,8 +721,8 @@ def assemble_taud(mod: dict) -> bytes:
|
|||||||
comp_size = len(compressed)
|
comp_size = len(compressed)
|
||||||
|
|
||||||
speed, tempo = find_initial_bpm_speed(patterns, order_list)
|
speed, tempo = find_initial_bpm_speed(patterns, order_list)
|
||||||
tempo = max(24, min(280, tempo))
|
tempo = max(25, min(280, tempo))
|
||||||
bpm_stored = (tempo - 24) & 0xFF
|
bpm_stored = (tempo - 25) & 0xFF
|
||||||
vprint(f" initial speed={speed}, tempo(BPM)={tempo}")
|
vprint(f" initial speed={speed}, tempo(BPM)={tempo}")
|
||||||
|
|
||||||
song_offset = TAUD_HEADER_SIZE + comp_size + TAUD_SONG_ENTRY
|
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
|
song_offset = TAUD_HEADER_SIZE + comp_size + TAUD_SONG_ENTRY
|
||||||
|
|
||||||
# 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 - 25
|
||||||
# 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 is fixed engine-wide to the equal-energy (no flag). Monotone has no
|
# 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") —
|
# 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 cmd == EFF_T:
|
||||||
if arg >= 0x20:
|
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.
|
# OpenMPT slide forms: $0y down per tick, $1y up per tick.
|
||||||
return (TOP_T, arg & 0xFF, None, None)
|
return (TOP_T, arg & 0xFF, None, None)
|
||||||
|
|
||||||
@@ -757,8 +757,8 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
|
|||||||
# Initial BPM / speed
|
# Initial BPM / speed
|
||||||
speed, tempo = find_initial_bpm_speed(patterns, h.order_list,
|
speed, tempo = find_initial_bpm_speed(patterns, h.order_list,
|
||||||
h.initial_speed, h.initial_tempo)
|
h.initial_speed, h.initial_tempo)
|
||||||
tempo = max(24, min(280, tempo))
|
tempo = max(25, min(280, tempo))
|
||||||
bpm_stored = (tempo - 24) & 0xFF
|
bpm_stored = (tempo - 25) & 0xFF
|
||||||
vprint(f" initial speed={speed}, tempo(BPM)={tempo}")
|
vprint(f" initial speed={speed}, tempo(BPM)={tempo}")
|
||||||
|
|
||||||
# Song offset = header(32) + compressed + song_table(8)
|
# Song offset = header(32) + compressed + song_table(8)
|
||||||
|
|||||||
@@ -2456,7 +2456,7 @@ Play Head Flags
|
|||||||
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.
|
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 (25 to 280. Play Data will change this register)
|
||||||
Byte 4 (Tracker Mode)
|
Byte 4 (Tracker Mode)
|
||||||
- Tick Rate (Play Data will change this register)
|
- Tick Rate (Play Data will change this register)
|
||||||
|
|
||||||
@@ -2486,8 +2486,8 @@ Play Head Flags
|
|||||||
Table of 3.5 Minifloat values (CSV).
|
Table of 3.5 Minifloat values (CSV).
|
||||||
Rebiased 2026-05-07 so the smallest non-zero step is 1/256 s and the maximum
|
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
|
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
|
tracker envelopes: a single song tick (≈ 8.9 ms at BPM 280, ≈ 100 ms at
|
||||||
BPM 24) now lands within ±17 % of an LUT entry across the whole supported
|
BPM 25) now lands within ±17 % of an LUT entry across the whole supported
|
||||||
BPM range; the previous bias was ±150 % at common BPMs.
|
BPM range; the previous bias was ±150 % at common BPMs.
|
||||||
,000,001,010,011,100,101,110,111,MSB
|
,000,001,010,011,100,101,110,111,MSB
|
||||||
00000,0,0.125,0.25,0.5,1,2,4,8
|
00000,0,0.125,0.25,0.5,1,2,4,8
|
||||||
@@ -2568,7 +2568,7 @@ Endianness: Little
|
|||||||
Uint32 Song offset
|
Uint32 Song offset
|
||||||
Uint8 Number of voices
|
Uint8 Number of voices
|
||||||
Uint16 Number of patterns (0 is invalid. pattern bin length = numPats * 8 bytes)
|
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)
|
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
|
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
|
||||||
@@ -2738,7 +2738,7 @@ The halt instruction (byte value 0x01 at cue offset 30) is placed on the last ac
|
|||||||
|
|
||||||
## Tempo mapping
|
## 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
|
## Global volume
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class AudioJSR223Delegate(private val vm: VM) {
|
|||||||
|
|
||||||
fun startSampleUpload(playhead: Int) { getPlayhead(playhead)?.pcmUpload = true }
|
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 getBPM(playhead: Int) = getPlayhead(playhead)?.bpm
|
||||||
|
|
||||||
fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 }
|
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
|
val hi = (rawArg ushr 8) and 0xFF
|
||||||
if (hi != 0) {
|
if (hi != 0) {
|
||||||
val tempoByte = hi
|
val tempoByte = hi
|
||||||
playhead.bpm = (tempoByte + 0x18).coerceIn(24, 280)
|
playhead.bpm = (tempoByte + 0x19).coerceIn(25, 280)
|
||||||
} else {
|
} else {
|
||||||
val low = rawArg and 0xFF
|
val low = rawArg and 0xFF
|
||||||
when (low and 0xF0) {
|
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).
|
// Tempo slide — applied once per tick at the playhead level (any channel that armed it).
|
||||||
for (voice in ts.voices) {
|
for (voice in ts.voices) {
|
||||||
if (voice.tempoSlideDir != 0 && ts.tickInRow > 0) {
|
if (voice.tempoSlideDir != 0 && ts.tickInRow > 0) {
|
||||||
val tempoByte = (playhead.bpm - 0x18 + voice.tempoSlideDir * voice.tempoSlideAmount).coerceIn(0, 0xFF)
|
val tempoByte = (playhead.bpm - 0x19 + voice.tempoSlideDir * voice.tempoSlideAmount).coerceIn(0, 0xFF)
|
||||||
playhead.bpm = (tempoByte + 0x18).coerceIn(24, 280)
|
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 masterVolume: Int = 0,
|
||||||
var masterPan: Int = 128,
|
var masterPan: Int = 128,
|
||||||
// var samplingRateMult: ThreeFiveMiniUfloat = ThreeFiveMiniUfloat(32),
|
// 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 tickRate: Int = 6,
|
||||||
var pcmUpload: Boolean = false,
|
var pcmUpload: Boolean = false,
|
||||||
var patBank1: Int = 0,
|
var patBank1: Int = 0,
|
||||||
@@ -3322,7 +3322,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
5 -> masterPan.toByte()
|
5 -> masterPan.toByte()
|
||||||
6 -> (isPcmMode.toInt(7) or isPlaying.toInt(4) or pcmQueueSizeIndex.and(15)).toByte()
|
6 -> (isPcmMode.toInt(7) or isPlaying.toInt(4) or pcmQueueSizeIndex.and(15)).toByte()
|
||||||
7 -> initialGlobalFlags.toByte()
|
7 -> initialGlobalFlags.toByte()
|
||||||
8 -> (bpm - 24).toByte()
|
8 -> (bpm - 25).toByte()
|
||||||
9 -> tickRate.toByte()
|
9 -> tickRate.toByte()
|
||||||
else -> throw InternalError("Bad offset $index")
|
else -> throw InternalError("Bad offset $index")
|
||||||
}
|
}
|
||||||
@@ -3352,16 +3352,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
ts.toneMode = byte and 3
|
ts.toneMode = byte and 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8 -> { bpm = byte + 24 }
|
8 -> { bpm = byte + 25 }
|
||||||
9 -> { tickRate = byte }
|
9 -> { tickRate = byte }
|
||||||
else -> throw InternalError("Bad offset $index")
|
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) {
|
fun setSamplingRate(rate: Int) {
|
||||||
val rateDiff = (rate.coerceIn(0, 95535) - 30000).toShort().toInt()
|
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)
|
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)
|
return (TOP_NONE, 0, None, None)
|
||||||
if arg < 0x20:
|
if arg < 0x20:
|
||||||
return (TOP_A, (arg & 0xFF) << 8, None, None)
|
return (TOP_A, (arg & 0xFF) << 8, None, None)
|
||||||
# Tempo: Taud T uses bias of -24 in stored form; mirror it2taud:
|
# Tempo: Taud T uses bias of -25 in stored form; mirror it2taud:
|
||||||
return (TOP_T, ((arg - 0x18) & 0xFF) << 8, None, None)
|
return (TOP_T, ((arg - 0x19) & 0xFF) << 8, None, None)
|
||||||
|
|
||||||
if cmd == 0x10:
|
if cmd == 0x10:
|
||||||
# Set global volume 0..64 → Taud V (×4 to fit 0..255).
|
# 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
|
# 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
|
# 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).
|
# (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)
|
ticks_per_sec = max(1.0, tempo_for_envs * 2.0 / 5.0)
|
||||||
|
|
||||||
# ── Build XM-instrument → list of Taud slot proxies ─────────────────────
|
# ── Build XM-instrument → list of Taud slot proxies ─────────────────────
|
||||||
@@ -1302,8 +1302,8 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes:
|
|||||||
# ── Tempo / speed ───────────────────────────────────────────────────────
|
# ── Tempo / speed ───────────────────────────────────────────────────────
|
||||||
speed = h.default_speed if h.default_speed > 0 else 6
|
speed = h.default_speed if h.default_speed > 0 else 6
|
||||||
tempo = h.default_bpm if h.default_bpm > 0 else 125
|
tempo = h.default_bpm if h.default_bpm > 0 else 125
|
||||||
tempo = max(24, min(280, tempo))
|
tempo = max(25, min(280, tempo))
|
||||||
bpm_stored = (tempo - 24) & 0xFF
|
bpm_stored = (tempo - 25) & 0xFF
|
||||||
vprint(f" initial speed={speed}, tempo={tempo} BPM")
|
vprint(f" initial speed={speed}, tempo={tempo} BPM")
|
||||||
|
|
||||||
# ── Channels / cue list ─────────────────────────────────────────────────
|
# ── Channels / cue list ─────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user