From 6eb73355caff5ba748b2ec82791298eeb64e708f Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 20 Jun 2026 04:00:33 +0900 Subject: [PATCH] taud: extended tempo range --- TAUD_NOTE_EFFECTS.md | 6 ++++ assets/disk0/tvdos/bin/monplay.js | 17 ++++++++-- assets/disk0/tvdos/bin/playtaud.js | 5 +-- assets/disk0/tvdos/bin/taut.js | 15 ++++---- assets/disk0/tvdos/include/taud.mjs | 10 +++--- midi2taud.py | 27 +++++++++------ taud_common.py | 9 +++-- taud_inspect.py | 4 +-- terranmon.txt | 21 ++++++++---- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 2 +- .../torvald/tsvm/peripheral/AudioAdapter.kt | 34 ++++++++++++------- 11 files changed, 101 insertions(+), 49 deletions(-) diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index 5b86d70..9ae83b5 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -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. +### 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 **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. diff --git a/assets/disk0/tvdos/bin/monplay.js b/assets/disk0/tvdos/bin/monplay.js index 21436c8..b793d2e 100644 --- a/assets/disk0/tvdos/bin/monplay.js +++ b/assets/disk0/tvdos/bin/monplay.js @@ -23,7 +23,7 @@ const BEEP_P_LO = -96 // MMIO 95: pppppp_QQ const BEEP_A = -97 // MMIO 96: A 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 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)) } +function cmdToInt(cmd) { + return cmd[0] | (cmd[1] << 8) | (cmd[2] << 16) | (cmd[3] << 24); +} + // --------------------------------------------------------------------------- // Render loop // --------------------------------------------------------------------------- @@ -322,6 +326,8 @@ const checkStop = () => { return stopReq } +let oldDiv = 0xFFFFFFFF + try { let o = 0 let startRow = 0 @@ -352,12 +358,19 @@ try { } 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)} ` + describeCommand(cmd, swInfo)) + } + globalTick++ nextTick += TICK_NANO + + oldDiv = cmdInt sleepUntil(nextTick) } diff --git a/assets/disk0/tvdos/bin/playtaud.js b/assets/disk0/tvdos/bin/playtaud.js index 2922c27..328470b 100644 --- a/assets/disk0/tvdos/bin/playtaud.js +++ b/assets/disk0/tvdos/bin/playtaud.js @@ -158,8 +158,9 @@ function parseTaud(path, songIndex) { const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) | ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8) - const bpm = (sys.peek(ptr + entryOff + 7) & 0xFF) + 25 - const tickRate = sys.peek(ptr + entryOff + 8) & 0xFF + const tickPacked = 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 cueCompSize = _peekU32LE(ptr, entryOff + 22) diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 74648b7..a3f88ec 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -826,8 +826,9 @@ function loadTaud(filePath, songIndex) { const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) | ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8) - const bpmStored = sys.peek(ptr + entryOff + 7) & 0xFF - const tickRate = sys.peek(ptr + entryOff + 8) & 0xFF + const tickPacked = 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 cueSheetCompSize = _peekU32LE(ptr, entryOff + 22) @@ -914,8 +915,8 @@ function loadTaudSongList(filePath) { numVoices: sys.peek(ptr + entryOff + 4) & 0xFF, numPats: (sys.peek(ptr + entryOff + 5) & 0xFF) | ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8), - bpm: ((sys.peek(ptr + entryOff + 7) & 0xFF) + 25), - tickRate: sys.peek(ptr + entryOff + 8) & 0xFF, + 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) & 0x7F, mixerflags: sys.peek(ptr + entryOff + 15) & 0xFF, songGlobalVolume: sys.peek(ptr + entryOff + 16) & 0xFF, songMixingVolume: sys.peek(ptr + entryOff + 17) & 0xFF, @@ -2726,8 +2727,10 @@ function simulateRowState(ptnDat, uptoRow) { } else if (effop === OP_T) { const hi = (effarg >>> 8) & 0xFF - if (hi !== 0) { - bpm = Math.max(25, Math.min(280, hi + 0x19)) + if (hi === 0xFF) { + 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 { const low = effarg & 0xFF if ((low & 0xF0) === 0x00 || (low & 0xF0) === 0x10) memTSlide = low diff --git a/assets/disk0/tvdos/include/taud.mjs b/assets/disk0/tvdos/include/taud.mjs index fc51bd0..2d00ae5 100644 --- a/assets/disk0/tvdos/include/taud.mjs +++ b/assets/disk0/tvdos/include/taud.mjs @@ -139,7 +139,9 @@ function uploadTaudFile(inFile, songIndex, playhead) { let numPatsLo = sys.peek(filePtr + entryOff + 5) & 0xFF let numPatsHi = sys.peek(filePtr + entryOff + 6) & 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 songGlobalVolume = sys.peek(filePtr + entryOff + 16) & 0xFF let songMixingVolume = sys.peek(filePtr + entryOff + 17) & 0xFF @@ -299,7 +301,7 @@ function captureTrackerDataToFile(outFile) { // -- 3. BPM / tick-rate / volumes from playhead 0 ------------------------- let bpm = audio.getBPM(0) || 125 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 songMixingVolume = audio.getSongMixingVolume(0) if (songGlobalVolume === undefined || songGlobalVolume === null) songGlobalVolume = 0x80 @@ -393,8 +395,8 @@ function captureTrackerDataToFile(outFile) { (songOffset >>> 24) & 0xFF, 20, // numVoices numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE - bpmStored, // BPM with −25 bias - tickRate, // initial tick-rate + bpmStored & 0xFF, // BPM with −25 bias (low 8 bits) + (((bpmStored >> 8) & 1) << 7) | (tickRate & 0x7F), // bit 7 = BPM high bit; bits 0..6 = tick-rate 0x00,0xA0, // basenote (0xA000 -- C9) 0x00,0xAC,0x02,0x46, // basefreq (8363 Hz) sys.peek(baseAddr - 7), // mixer flags diff --git a/midi2taud.py b/midi2taud.py index 69bb42b..e03eddf 100644 --- a/midi2taud.py +++ b/midi2taud.py @@ -72,7 +72,7 @@ Behaviour (per midi2taud.md): 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 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 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 @@ -80,7 +80,8 @@ Behaviour (per midi2taud.md): 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- 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 never disturb the velocity-driven patch selection axis. * 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" # sub-row + tempo precision, so the picker spends it rather than minimising F. _F_TARGET = 24 -# Taud BPM register is bias-25 in [25, 280]; tick rate Hz = bpm·2/5. -_TAUD_BPM_LO, _TAUD_BPM_HI = 25, 280 +# 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, 535 # 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, @@ -2477,10 +2478,16 @@ def emit_cells(song: Song, insts: dict, speed: int, rpb: int, def taud_bpm(b): 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}, " - f"clamped to 25..280 (try a different --rpb/--speed)") - return max(25, min(280, t)) + f"clamped to {_TAUD_BPM_LO}..{_TAUD_BPM_HI} (try a different --rpb/--speed)") + 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) if n_voices == 0: @@ -2663,7 +2670,7 @@ def emit_cells(song: Song, insts: dict, speed: int, rpb: int, c = cells.get((v, row)) if c is None or c['eff'] is None: c = _cell(cells, v, row) - c['eff'] = (TOP_T, ((tb - 25) & 0xFF) << 8) + c['eff'] = (TOP_T, tempo_effarg(tb)) c['prio'] = PRIO_TEMPO placed = True 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 victim['prio'] == PRIO_PORTA: 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 placed = True t_evict += 1 @@ -3008,7 +3015,7 @@ def make_song_entry(section: dict, song_off: int, args) -> bytes: song_offset=song_off, num_voices=section['n_voices'], 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'], base_note=0xA000, base_freq=8363.0, diff --git a/taud_common.py b/taud_common.py index 0d027b6..e4fd60d 100644 --- a/taud_common.py +++ b/taud_common.py @@ -452,18 +452,23 @@ def encode_song_entry(song_offset: int, num_voices: int, num_patterns: int, Layout: 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, u8 flags, u8 global_vol, u8 mixing_vol, u32 pat_bin_comp_size, u32 cue_sheet_comp_size, 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('> 8) & 1) << 7) | (tick_rate & 0x7F), base_note & 0xFFFF, float(base_freq), flags_byte & 0xFF, diff --git a/taud_inspect.py b/taud_inspect.py index 124668f..60720c8 100644 --- a/taud_inspect.py +++ b/taud_inspect.py @@ -780,8 +780,8 @@ def main(): soff = u32(data, eoff) nvoices = data[eoff + 4] npats = u16(data, eoff + 5) - bpm = data[eoff + 7] + 25 - tickrate = data[eoff + 8] + bpm = data[eoff + 7] + 25 + ((data[eoff + 8] & 0x80) << 1) # bit 7 of byte 8 = BPM high bit + tickrate = data[eoff + 8] & 0x7F tuning_base = u16(data, eoff + 9) base_freq = struct.unpack_from('>> 1)) 10: two-note argeggio (rate = 60 Hz) tick 1: base note at divisor P 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 2: second note at divisor (P - A) 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) @@ -2942,7 +2946,10 @@ TODO: 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. [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: * 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes @@ -3199,7 +3206,7 @@ Endianness: Little Uint8 Number of voices 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 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 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') diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index b131a3c..e57f99a 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -89,7 +89,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 - 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 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 9d992ad..ff29aa6 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -3370,14 +3370,21 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { EffectOp.OP_S -> applySEffect(ts, voice, vi, rawArg) EffectOp.OP_T -> { val hi = (rawArg ushr 8) and 0xFF - if (hi != 0) { - val tempoByte = hi - playhead.bpm = (tempoByte + 0x19).coerceIn(25, 280) - } 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 } + when { + hi == 0xFF -> { + // T $FFxx — extended set-tempo: BPM = $xx + $118 (280..535). See TAUD_NOTE_EFFECTS.md §T $FFxx. + playhead.bpm = ((rawArg and 0xFF) + 0x118).coerceIn(25, 535) + } + hi != 0 -> { + // T $xx00 — set-tempo: BPM = $xx + $19 (25..280). + 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 masterPan: Int = 128, // 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 pcmUpload: Boolean = false, var patBank1: Int = 0, @@ -4833,8 +4840,9 @@ 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 - 25).toByte() - 9 -> tickRate.toByte() + 8 -> ((bpm - 25) and 0xFF).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") } @@ -4861,8 +4869,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { initialGlobalFlags = byte updateTrackerGlobalBehaviour(initialGlobalFlags) } - 8 -> { bpm = byte + 25 } - 9 -> { tickRate = byte } + 8 -> { bpm = (((bpm - 25) and 0x100) or byte) + 25 } // low 8 bits of bpm-25, preserve high bit + 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") } }