taud: extended tempo range

This commit is contained in:
minjaesong
2026-06-20 04:00:33 +09:00
parent 1173373789
commit 6eb73355ca
11 changed files with 101 additions and 49 deletions

View File

@@ -709,6 +709,12 @@ ProTracker `Fxx` with `xx ≥ $20` maps to Taud `T $(xx $19)00`; `Fxx` with
**Implementation.** If the high byte is non-zero, set `tempo_byte = arg >> 8`; derive `BPM = tempo_byte + $19`; compute tick duration as `samples_per_tick = 32000 × 5 / (BPM × 2) = 80000 / BPM` (integer truncated) at the fixed 32000 Hz output rate. Example: BPM 125 → 640 samples per tick; BPM 24 → 3200 samples per tick; BPM 280 → 286 samples per tick. There is no memory for set-tempo. **Implementation.** If the high byte is non-zero, set `tempo_byte = arg >> 8`; derive `BPM = tempo_byte + $19`; compute tick duration as `samples_per_tick = 32000 × 5 / (BPM × 2) = 80000 / BPM` (integer truncated) at the fixed 32000 Hz output rate. Example: BPM 125 → 640 samples per tick; BPM 24 → 3200 samples per tick; BPM 280 → 286 samples per tick. There is no memory for set-tempo.
### T $FFxx (high byte 0xFF) — Set tempo (extended)
**Plain.** Sets the Taud tempo byte to `$FF + $xx`. The resulting BPM is `$xx + $118`: xx = $00 → 280 BPM, $64 → 380 BPM, $FF → 535 BPM.
**Compatibility.** Unique to Taud.
### T $00xy (high byte zero) — Tempo slide ### T $00xy (high byte zero) — Tempo slide
**Plain.** Adjusts the tempo continuously during the row. `$00_0y` (low nibble under a zero high nibble within the low byte) slides BPM down by `$y` per non-first tick; `$00_1y` slides up. Out-of-range encodings ($00_20 through $00_FF) are reserved and behave as no-ops. **Plain.** Adjusts the tempo continuously during the row. `$00_0y` (low nibble under a zero high nibble within the low byte) slides BPM down by `$y` per non-first tick; `$00_1y` slides up. Out-of-range encodings ($00_20 through $00_FF) are reserved and behave as no-ops.

View File

@@ -23,7 +23,7 @@ const BEEP_P_LO = -96 // MMIO 95: pppppp_QQ
const BEEP_A = -97 // MMIO 96: A const BEEP_A = -97 // MMIO 96: A
const BEEP_B = -98 // MMIO 97: B const BEEP_B = -98 // MMIO 97: B
const BEEP_HALFCLOCK = 3579545 / 16 / 2 // f = BEEP_HALFCLOCK / divider const BEEP_HALFCLOCK = (3579545.4545454545 / 16.0) / 2 // f = BEEP_HALFCLOCK / divider
const DIVIDER_MAX = 0x3FFF // 14-bit const DIVIDER_MAX = 0x3FFF // 14-bit
const QQ_NONE = 0, QQ_TWO = 2, QQ_THREE = 3 // beeper note-effect (QQ field) const QQ_NONE = 0, QQ_TWO = 2, QQ_THREE = 3 // beeper note-effect (QQ field)
@@ -308,6 +308,10 @@ function applyTickEffects(v, t) {
const sleepUntil = (nano) => { const ms = (nano - sys.nanoTime()) / 1e6; if (ms >= 1) sys.sleep(Math.floor(ms)) } const sleepUntil = (nano) => { const ms = (nano - sys.nanoTime()) / 1e6; if (ms >= 1) sys.sleep(Math.floor(ms)) }
function cmdToInt(cmd) {
return cmd[0] | (cmd[1] << 8) | (cmd[2] << 16) | (cmd[3] << 24);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Render loop // Render loop
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -322,6 +326,8 @@ const checkStop = () => {
return stopReq return stopReq
} }
let oldDiv = 0xFFFFFFFF
try { try {
let o = 0 let o = 0
let startRow = 0 let startRow = 0
@@ -352,12 +358,19 @@ try {
} }
uploadBeeper(cmd[0], cmd[1], cmd[2], cmd[3]) uploadBeeper(cmd[0], cmd[1], cmd[2], cmd[3])
println(`${String(globalTick).padStart(6, '0')} ` + let cmdInt = cmdToInt(cmd)
if (oldDiv != cmdInt) {
println(`${String(globalTick).padStart(6, '0')} ` +
`c${String(o).padStart(2)} r${String(row).padStart(2)} t${String(t).padStart(2)} ` + `c${String(o).padStart(2)} r${String(row).padStart(2)} t${String(t).padStart(2)} ` +
describeCommand(cmd, swInfo)) describeCommand(cmd, swInfo))
}
globalTick++ globalTick++
nextTick += TICK_NANO nextTick += TICK_NANO
oldDiv = cmdInt
sleepUntil(nextTick) sleepUntil(nextTick)
} }

View File

@@ -158,8 +158,9 @@ function parseTaud(path, songIndex) {
const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF
const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) | const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) |
((sys.peek(ptr + entryOff + 6) & 0xFF) << 8) ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8)
const bpm = (sys.peek(ptr + entryOff + 7) & 0xFF) + 25 const tickPacked = sys.peek(ptr + entryOff + 8) & 0xFF
const tickRate = sys.peek(ptr + entryOff + 8) & 0xFF const bpm = (sys.peek(ptr + entryOff + 7) & 0xFF) + 25 + ((tickPacked & 0x80) << 1) // bit 7 = BPM high bit
const tickRate = tickPacked & 0x7F
const patCompSize = _peekU32LE(ptr, entryOff + 18) const patCompSize = _peekU32LE(ptr, entryOff + 18)
const cueCompSize = _peekU32LE(ptr, entryOff + 22) const cueCompSize = _peekU32LE(ptr, entryOff + 22)

View File

@@ -826,8 +826,9 @@ function loadTaud(filePath, songIndex) {
const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF
const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) | const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) |
((sys.peek(ptr + entryOff + 6) & 0xFF) << 8) ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8)
const bpmStored = sys.peek(ptr + entryOff + 7) & 0xFF const tickPacked = sys.peek(ptr + entryOff + 8) & 0xFF
const tickRate = sys.peek(ptr + entryOff + 8) & 0xFF const bpmStored = (sys.peek(ptr + entryOff + 7) & 0xFF) | ((tickPacked & 0x80) << 1) // bit 7 of byte 8 = BPM high bit
const tickRate = tickPacked & 0x7F
const patBinCompSize = _peekU32LE(ptr, entryOff + 18) const patBinCompSize = _peekU32LE(ptr, entryOff + 18)
const cueSheetCompSize = _peekU32LE(ptr, entryOff + 22) const cueSheetCompSize = _peekU32LE(ptr, entryOff + 22)
@@ -914,8 +915,8 @@ function loadTaudSongList(filePath) {
numVoices: sys.peek(ptr + entryOff + 4) & 0xFF, numVoices: sys.peek(ptr + entryOff + 4) & 0xFF,
numPats: (sys.peek(ptr + entryOff + 5) & 0xFF) | numPats: (sys.peek(ptr + entryOff + 5) & 0xFF) |
((sys.peek(ptr + entryOff + 6) & 0xFF) << 8), ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8),
bpm: ((sys.peek(ptr + entryOff + 7) & 0xFF) + 25), bpm: ((sys.peek(ptr + entryOff + 7) & 0xFF) + 25 + ((sys.peek(ptr + entryOff + 8) & 0x80) << 1)), // bit 7 of byte 8 = BPM high bit
tickRate: sys.peek(ptr + entryOff + 8) & 0xFF, tickRate: sys.peek(ptr + entryOff + 8) & 0x7F,
mixerflags: sys.peek(ptr + entryOff + 15) & 0xFF, mixerflags: sys.peek(ptr + entryOff + 15) & 0xFF,
songGlobalVolume: sys.peek(ptr + entryOff + 16) & 0xFF, songGlobalVolume: sys.peek(ptr + entryOff + 16) & 0xFF,
songMixingVolume: sys.peek(ptr + entryOff + 17) & 0xFF, songMixingVolume: sys.peek(ptr + entryOff + 17) & 0xFF,
@@ -2726,8 +2727,10 @@ 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 === 0xFF) {
bpm = Math.max(25, Math.min(280, hi + 0x19)) bpm = Math.max(25, Math.min(535, (effarg & 0xFF) + 0x118)) // T $FFxx — extended tempo
} else if (hi !== 0) {
bpm = Math.max(25, Math.min(535, 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

View File

@@ -139,7 +139,9 @@ function uploadTaudFile(inFile, songIndex, playhead) {
let numPatsLo = sys.peek(filePtr + entryOff + 5) & 0xFF let numPatsLo = sys.peek(filePtr + entryOff + 5) & 0xFF
let numPatsHi = sys.peek(filePtr + entryOff + 6) & 0xFF let numPatsHi = sys.peek(filePtr + entryOff + 6) & 0xFF
let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF
let tickRate = sys.peek(filePtr + entryOff + 8) & 0xFF let tickPacked = sys.peek(filePtr + entryOff + 8) & 0xFF
let tickRate = tickPacked & 0x7F // bits 0..6
bpmStored |= (tickPacked & 0x80) << 1 // bit 7 of byte 8 = BPM high bit (0x100..0x1FE)
let mixerflags = sys.peek(filePtr + entryOff + 15) & 0xFF let mixerflags = sys.peek(filePtr + entryOff + 15) & 0xFF
let songGlobalVolume = sys.peek(filePtr + entryOff + 16) & 0xFF let songGlobalVolume = sys.peek(filePtr + entryOff + 16) & 0xFF
let songMixingVolume = sys.peek(filePtr + entryOff + 17) & 0xFF let songMixingVolume = sys.peek(filePtr + entryOff + 17) & 0xFF
@@ -299,7 +301,7 @@ function captureTrackerDataToFile(outFile) {
// -- 3. BPM / tick-rate / volumes from playhead 0 ------------------------- // -- 3. BPM / tick-rate / volumes from playhead 0 -------------------------
let bpm = audio.getBPM(0) || 125 let bpm = audio.getBPM(0) || 125
let tickRate = audio.getTickRate(0) || 6 let tickRate = audio.getTickRate(0) || 6
let bpmStored = (bpm - 25) & 0xFF let bpmStored = Math.max(0, Math.min(0x1FE, bpm - 25)) // 9-bit (0..510 ⇒ BPM 25..535)
let songGlobalVolume = audio.getSongGlobalVolume(0) let songGlobalVolume = audio.getSongGlobalVolume(0)
let songMixingVolume = audio.getSongMixingVolume(0) let songMixingVolume = audio.getSongMixingVolume(0)
if (songGlobalVolume === undefined || songGlobalVolume === null) songGlobalVolume = 0x80 if (songGlobalVolume === undefined || songGlobalVolume === null) songGlobalVolume = 0x80
@@ -393,8 +395,8 @@ function captureTrackerDataToFile(outFile) {
(songOffset >>> 24) & 0xFF, (songOffset >>> 24) & 0xFF,
20, // numVoices 20, // numVoices
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
bpmStored, // BPM with 25 bias bpmStored & 0xFF, // BPM with 25 bias (low 8 bits)
tickRate, // initial tick-rate (((bpmStored >> 8) & 1) << 7) | (tickRate & 0x7F), // bit 7 = BPM high bit; bits 0..6 = tick-rate
0x00,0xA0, // basenote (0xA000 -- C9) 0x00,0xA0, // basenote (0xA000 -- C9)
0x00,0xAC,0x02,0x46, // basefreq (8363 Hz) 0x00,0xAC,0x02,0x46, // basefreq (8363 Hz)
sys.peek(baseAddr - 7), // mixer flags sys.peek(baseAddr - 7), // mixer flags

View File

@@ -72,7 +72,7 @@ Behaviour (per midi2taud.md):
default from the tempo map, the MIDI time signatures and onset-subdivision default from the tempo map, the MIDI time signatures and onset-subdivision
analysis: rpb·speed fine-ticks per beat is chosen to represent the finest analysis: rpb·speed fine-ticks per beat is chosen to represent the finest
subdivision actually used, keep every tempo inside the Taud BPM register subdivision actually used, keep every tempo inside the Taud BPM register
(25..280), and stay near the proven 24-fts/beat grid — so plain 4/4 @ 120 (25..535), and stay near the proven 24-fts/beat grid — so plain 4/4 @ 120
BPM still reproduces the old speed 6 / rpb 4. Passing --rpb or --speed pins BPM still reproduces the old speed 6 / rpb 4. Passing --rpb or --speed pins
that axis and auto-fits the other; pass both to fully override. As a final that axis and auto-fits the other; pass both to fully override. As a final
step, a bend- or polyphony-heavy song with rpb < 8 has its rpb doubled (and step, a bend- or polyphony-heavy song with rpb < 8 has its rpb doubled (and
@@ -80,7 +80,8 @@ Behaviour (per midi2taud.md):
key-offs, exclusiveClass chokes, bend portamento (G) and channel-volume (M) key-offs, exclusiveClass chokes, bend portamento (G) and channel-volume (M)
effects more distinct rows to land on, so fewer are eaten by same-row / per- effects more distinct rows to land on, so fewer are eaten by same-row / per-
cell-slot collisions. Disabled by pinning --rpb or --speed. cell-slot collisions. Disabled by pinning --rpb or --speed.
MIDI tempo changes map to T $xx00 set-tempo effects; channel volume / MIDI tempo changes map to T $xx00 set-tempo effects (or T $FFxx extended
set-tempo above 280 BPM); channel volume /
expression (CC7 × CC11) map to M $xx00 channel-volume effects so they expression (CC7 × CC11) map to M $xx00 channel-volume effects so they
never disturb the velocity-driven patch selection axis. never disturb the velocity-driven patch selection axis.
* Cues are broken at every time-signature change, and each section is packed * Cues are broken at every time-signature change, and each section is packed
@@ -569,8 +570,8 @@ _SUBDIV_THRESHOLD = 0.95
# NOTE: row/pattern count depends only on rpb (rows = beats×rpb); speed is "free" # NOTE: row/pattern count depends only on rpb (rows = beats×rpb); speed is "free"
# sub-row + tempo precision, so the picker spends it rather than minimising F. # sub-row + tempo precision, so the picker spends it rather than minimising F.
_F_TARGET = 24 _F_TARGET = 24
# Taud BPM register is bias-25 in [25, 280]; tick rate Hz = bpm·2/5. # Taud BPM register is bias-25 in [25, 535] (T $FFxx extends past 280); tick rate Hz = bpm·2/5.
_TAUD_BPM_LO, _TAUD_BPM_HI = 25, 280 _TAUD_BPM_LO, _TAUD_BPM_HI = 25, 535
# RPB bump: bend- or polyphony-heavy songs cram more triggers / key-offs / chokes # RPB bump: bend- or polyphony-heavy songs cram more triggers / key-offs / chokes
# / bend-G / channel-M into each beat than emit_cells can place on distinct rows, # / bend-G / channel-M into each beat than emit_cells can place on distinct rows,
@@ -2477,10 +2478,16 @@ def emit_cells(song: Song, insts: dict, speed: int, rpb: int,
def taud_bpm(b): def taud_bpm(b):
t = round(b * scale) t = round(b * scale)
if not (25 <= t <= 280): if not (_TAUD_BPM_LO <= t <= _TAUD_BPM_HI):
vprint(f" warning: tempo {b:.1f} BPM maps to Taud {t}, " vprint(f" warning: tempo {b:.1f} BPM maps to Taud {t}, "
f"clamped to 25..280 (try a different --rpb/--speed)") f"clamped to {_TAUD_BPM_LO}..{_TAUD_BPM_HI} (try a different --rpb/--speed)")
return max(25, min(280, t)) return max(_TAUD_BPM_LO, min(_TAUD_BPM_HI, t))
def tempo_effarg(tb):
# T $xx00 set-tempo (BPM = xx+$19) up to 280; T $FFxx extended (BPM = xx+$118) above.
if tb <= 280:
return ((tb - 25) & 0xFF) << 8
return 0xFF00 | ((tb - 280) & 0xFF)
n_voices = allocate_voices(notes, speed, max_voices) n_voices = allocate_voices(notes, speed, max_voices)
if n_voices == 0: if n_voices == 0:
@@ -2663,7 +2670,7 @@ def emit_cells(song: Song, insts: dict, speed: int, rpb: int,
c = cells.get((v, row)) c = cells.get((v, row))
if c is None or c['eff'] is None: if c is None or c['eff'] is None:
c = _cell(cells, v, row) c = _cell(cells, v, row)
c['eff'] = (TOP_T, ((tb - 25) & 0xFF) << 8) c['eff'] = (TOP_T, tempo_effarg(tb))
c['prio'] = PRIO_TEMPO c['prio'] = PRIO_TEMPO
placed = True placed = True
break break
@@ -2673,7 +2680,7 @@ def emit_cells(song: Song, insts: dict, speed: int, rpb: int,
if not placed and victim is not None: if not placed and victim is not None:
if victim['prio'] == PRIO_PORTA: if victim['prio'] == PRIO_PORTA:
victim['note'] = NOTE_NOP # orphan G note would retrigger victim['note'] = NOTE_NOP # orphan G note would retrigger
victim['eff'] = (TOP_T, ((tb - 25) & 0xFF) << 8) victim['eff'] = (TOP_T, tempo_effarg(tb))
victim['prio'] = PRIO_TEMPO victim['prio'] = PRIO_TEMPO
placed = True placed = True
t_evict += 1 t_evict += 1
@@ -3008,7 +3015,7 @@ def make_song_entry(section: dict, song_off: int, args) -> bytes:
song_offset=song_off, song_offset=song_off,
num_voices=section['n_voices'], num_voices=section['n_voices'],
num_patterns=section['n_unique'], num_patterns=section['n_unique'],
bpm_stored=(section['bpm0'] - 25) & 0xFF, bpm_stored=section['bpm0'] - 25, # 9-bit; encode_song_entry packs bit 8 into byte-8 bit 7
tick_rate=section['speed'], tick_rate=section['speed'],
base_note=0xA000, base_note=0xA000,
base_freq=8363.0, base_freq=8363.0,

View File

@@ -452,18 +452,23 @@ def encode_song_entry(song_offset: int, num_voices: int, num_patterns: int,
Layout: Layout:
u32 song_offset, u8 num_voices, u16 num_patterns, u32 song_offset, u8 num_voices, u16 num_patterns,
u8 bpm_stored, u8 tick_rate, u8 bpm_stored, u8 bpm_hi_bit<<7 | tick_rate,
u16 base_note, f32 base_freq, u16 base_note, f32 base_freq,
u8 flags, u8 global_vol, u8 mixing_vol, u8 flags, u8 global_vol, u8 mixing_vol,
u32 pat_bin_comp_size, u32 cue_sheet_comp_size, u32 pat_bin_comp_size, u32 cue_sheet_comp_size,
byte[6] reserved. byte[6] reserved.
`bpm_stored` is `bpm - 25` and may be a 9-bit value (0..510 ⇒ BPM 25..535);
its low 8 bits go to the bpm byte and bit 8 is packed into bit 7 of the
tick-rate byte (which therefore caps tick_rate at 0..127). See terranmon.txt.
""" """
bpm_stored = max(0, min(0x1FE, bpm_stored))
entry = struct.pack('<IBHBBHfBBBII', entry = struct.pack('<IBHBBHfBBBII',
song_offset, song_offset,
num_voices & 0xFF, num_voices & 0xFF,
num_patterns & 0xFFFF, num_patterns & 0xFFFF,
bpm_stored & 0xFF, bpm_stored & 0xFF,
tick_rate & 0xFF, (((bpm_stored >> 8) & 1) << 7) | (tick_rate & 0x7F),
base_note & 0xFFFF, base_note & 0xFFFF,
float(base_freq), float(base_freq),
flags_byte & 0xFF, flags_byte & 0xFF,

View File

@@ -780,8 +780,8 @@ def main():
soff = u32(data, eoff) soff = u32(data, eoff)
nvoices = data[eoff + 4] nvoices = data[eoff + 4]
npats = u16(data, eoff + 5) npats = u16(data, eoff + 5)
bpm = data[eoff + 7] + 25 bpm = data[eoff + 7] + 25 + ((data[eoff + 8] & 0x80) << 1) # bit 7 of byte 8 = BPM high bit
tickrate = data[eoff + 8] tickrate = data[eoff + 8] & 0x7F
tuning_base = u16(data, eoff + 9) tuning_base = u16(data, eoff + 9)
base_freq = struct.unpack_from('<f', data, eoff + 11)[0] base_freq = struct.unpack_from('<f', data, eoff + 11)[0]
gbflags = data[eoff + 15] gbflags = data[eoff + 15]

View File

@@ -119,14 +119,13 @@ MMIO
93 RO: Set beeper status (aka upload beeper command) 93 RO: Set beeper status (aka upload beeper command)
READING causes the side effect (and returns beeper status — 1 if a tone is currently sounding, 0 otherwise). WRITING DOES NOTHING READING causes the side effect (and returns beeper status — 1 if a tone is currently sounding, 0 otherwise). WRITING DOES NOTHING
94..97 RW: Beeper command 94..99 RW: Beeper command
0bPPPPPPPP 0bpppppp_QQ 0bAAAAAAAA 0bBBBBBBBB 0bPPPPPPPP 0bpppppp_QQ 0bqqAABBCC 0baaaaaaaa 0bbbbbbbbb 0bcccccccc
PPPPPPPPpppppp: frequency divider (master clock: 3579545 / 16 Hz), determines pitch. PPPPPPPPpppppp: 14-bit frequency divider (master clock: 3579545 / 16 Hz), determines pitch.
0: no sound 0: no sound
QQ: note effect QQ: note effect
00: none 00: none
01: fixed arpeggio (rate = 60 Hz, second note is always divisor (P >>> 1))
10: two-note argeggio (rate = 60 Hz) 10: two-note argeggio (rate = 60 Hz)
tick 1: base note at divisor P is played tick 1: base note at divisor P is played
tick 2: second note at divisor (P - (B << 8 | A)) is played tick 2: second note at divisor (P - (B << 8 | A)) is played
@@ -134,7 +133,12 @@ MMIO
tick 1: base note at divisor P is played tick 1: base note at divisor P is played
tick 2: second note at divisor (P - A) is played tick 2: second note at divisor (P - A) is played
tick 3: third note at divisor (P - A - B) is played tick 3: third note at divisor (P - A - B) is played
A/B: note effect arguments 01: four-note arpeggio (rate = 60 Hz)
tick 1: base note at divisor P is played
tick 2: second note at divisor (P - A) is played
tick 3: third note at divisor (P - A - B) is played
tick 4: fourth note at divisor (P - A - B - C) is played
Aa/Bb/Cc: note effect arguments (10-bit divisor delta for arpeggiator; byte 96 has high two bits)
1024..2047 RW: Reserved for integrated peripherals (e.g. built-in status display) 1024..2047 RW: Reserved for integrated peripherals (e.g. built-in status display)
@@ -2942,7 +2946,10 @@ TODO:
reset on retrigger, so a true value means this note was released. A parent that ended naturally reset on retrigger, so a true value means this note was released. A parent that ended naturally
(no release) still leaves the child to finish on its own. (no release) still leaves the child to finish on its own.
[x] Some ways to decouple Sample+Inst and patterns into separate files (tsvm-doom needs separate file access; samplepack can be uploaded once on init) [x] Some ways to decouple Sample+Inst and patterns into separate files (tsvm-doom needs separate file access; samplepack can be uploaded once on init)
[ ] The same VT-aware patch thing for all fullscreen apps, possibly made simple by only requiring one row of simple code [x] The same VT-aware patch thing for all fullscreen apps, possibly made simple by only requiring one row of simple code
* DONE 2026-06-20. See `con.setFullscreen()`
[x] Taud double the BPM ceiling
* DONE 2026-06-20. BPM range 25..535 via `T $FFxx` + the song-table byte-8 high bit (tickrate now 7-bit).
TODO - list of demo songs that MUST ship with Microtone: TODO - list of demo songs that MUST ship with Microtone:
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes * 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
@@ -3199,7 +3206,7 @@ Endianness: Little
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 -25. 0x00=25, 0xFF=280) Uint8 Initial BPM (bias of -25. 0x00=25, 0xFF=280)
Uint8 Initial Tickrate (0 is invalid) Uint8 BPM high bit (bit 7; 0x100=281, 0x1FE = 535; `T FFxx` cannot put 0x1FF into BPM) + initial Tickrate (0 is invalid; bit 0..6)
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')

View File

@@ -89,7 +89,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 - 25).and(255) + 25 } fun setBPM(playhead: Int, bpm: Int) { getPlayhead(playhead)?.bpm = bpm.coerceIn(25, 535) }
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 }

View File

@@ -3370,14 +3370,21 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
EffectOp.OP_S -> applySEffect(ts, voice, vi, rawArg) EffectOp.OP_S -> applySEffect(ts, voice, vi, rawArg)
EffectOp.OP_T -> { EffectOp.OP_T -> {
val hi = (rawArg ushr 8) and 0xFF val hi = (rawArg ushr 8) and 0xFF
if (hi != 0) { when {
val tempoByte = hi hi == 0xFF -> {
playhead.bpm = (tempoByte + 0x19).coerceIn(25, 280) // T $FFxx — extended set-tempo: BPM = $xx + $118 (280..535). See TAUD_NOTE_EFFECTS.md §T $FFxx.
} else { playhead.bpm = ((rawArg and 0xFF) + 0x118).coerceIn(25, 535)
val low = rawArg and 0xFF }
when (low and 0xF0) { hi != 0 -> {
0x00 -> { voice.tempoSlideDir = -1; voice.tempoSlideAmount = low and 0x0F; voice.mem.tslide = low } // T $xx00 — set-tempo: BPM = $xx + $19 (25..280).
0x10 -> { voice.tempoSlideDir = +1; voice.tempoSlideAmount = low and 0x0F; voice.mem.tslide = low } playhead.bpm = (hi + 0x19).coerceIn(25, 535)
}
else -> {
val low = rawArg and 0xFF
when (low and 0xF0) {
0x00 -> { voice.tempoSlideDir = -1; voice.tempoSlideAmount = low and 0x0F; voice.mem.tslide = low }
0x10 -> { voice.tempoSlideDir = +1; voice.tempoSlideAmount = low and 0x0F; voice.mem.tslide = low }
}
} }
} }
} }
@@ -4778,7 +4785,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 + 25. Spec default $64 ⇒ 125 BPM. var bpm: Int = 125, // BPM, derived from tempoByte + 25. Spec default $64 ⇒ 125 BPM. Range 25..535 (T $FFxx extends past 280).
var tickRate: Int = 6, var tickRate: Int = 6,
var pcmUpload: Boolean = false, var pcmUpload: Boolean = false,
var patBank1: Int = 0, var patBank1: Int = 0,
@@ -4833,8 +4840,9 @@ 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 - 25).toByte() 8 -> ((bpm - 25) and 0xFF).toByte()
9 -> tickRate.toByte() // bit 7 = BPM high bit (bit 8 of bpm-25, for the 281..535 range); bits 0..6 = tickRate. See terranmon.txt §Taud song table.
9 -> (((((bpm - 25) ushr 8) and 1) shl 7) or (tickRate and 0x7F)).toByte()
else -> throw InternalError("Bad offset $index") else -> throw InternalError("Bad offset $index")
} }
@@ -4861,8 +4869,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
initialGlobalFlags = byte initialGlobalFlags = byte
updateTrackerGlobalBehaviour(initialGlobalFlags) updateTrackerGlobalBehaviour(initialGlobalFlags)
} }
8 -> { bpm = byte + 25 } 8 -> { bpm = (((bpm - 25) and 0x100) or byte) + 25 } // low 8 bits of bpm-25, preserve high bit
9 -> { tickRate = byte } 9 -> { tickRate = byte and 0x7F; bpm = (((byte and 0x80) shl 1) or ((bpm - 25) and 0xFF)) + 25 } // bit 7 -> bpm high bit
else -> throw InternalError("Bad offset $index") else -> throw InternalError("Bad offset $index")
} }
} }