mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-20 19:24:04 +09:00
taud: extended tempo range
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
27
midi2taud.py
27
midi2taud.py
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user