From 3c57e33f8fca5eba20f8c202ed5b5a2312a53774 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 9 May 2026 03:00:54 +0900 Subject: [PATCH] funk repeat OOB fix --- CLAUDE.md | 1 + TAUD_NOTE_EFFECTS.md | 4 ++-- terranmon.txt | 13 +++++++++++++ .../src/net/torvald/tsvm/peripheral/AudioAdapter.kt | 11 ++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bfe7e21..61cd4c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,7 @@ Current topics: - `reference_materials/impulse-tracker` — The original source code for ImpulseTracker - `reference_materials/MilkyTracker` — FastTracker 2 compatible tracker - `reference_materials/schismtracker` — Open-source re-implementation of ImpulseTracker +- `reference_materials/pt2-clone` — Open-source re-implementation of ProTracker 2 When fetching new references, copy the relevant upstream files verbatim into a topic folder, write a `README.md` summarising the relevant maths / diff --git a/TAUD_NOTE_EFFECTS.md b/TAUD_NOTE_EFFECTS.md index 21d1b4d..cb6b80a 100644 --- a/TAUD_NOTE_EFFECTS.md +++ b/TAUD_NOTE_EFFECTS.md @@ -1190,7 +1190,7 @@ There is no separate "use fadeout" flag — both extremes share the same field, This table maps each PT effect to its Taud equivalent. Arguments follow PT's two-nibble form and expand to Taud's 16-bit form as shown. | PT effect | Taud effect | Notes | -|---------|-----------|-------| +|---------|---------|-------| | `0 $xy` | `J $xxyy` | Arpeggio; nibble-repeat each byte. See the 12-TET → Taud table above for conversion losses | | `1 $xx` | `F $00xx` (Amiga mode, `f` set) | Portamento up; raw PT period units, applied in period space | | `2 $xx` | `E $00xx` (Amiga mode, `f` set) | Portamento down; raw PT period units, applied in period space | @@ -1220,7 +1220,7 @@ This table maps each PT effect to its Taud equivalent. Arguments follow PT's two | `E $Cx` | `S $Cx00` | Note cut | | `E $Dx` | `S $Dx00` | Note delay | | `E $Ex` | `S $Ex00` | Pattern delay | -| `E $Fx` | `S $Fx00` | Funk repeat | +| `E $Fx` | `S $Fyyy` | Funk repeat, where `yyy = funk_table[x]` | | `F $xx` (xx < $20) | `A $xx00` | Set speed | | `F $xx` (xx ≥ $20) | `T $(xx−$18)00` | Set tempo | diff --git a/terranmon.txt b/terranmon.txt index 20ca5a5..bfd9dc5 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2395,6 +2395,19 @@ TODO: updated; legacy `.taud` files (byte 196 == 0) fall back to the previous "row volume default = 63" behaviour. +TODO - list of demo songs that MUST ship with Microtone: + * 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes + (C) Skaven 1998 + * Slumberjack — for demonstrating XM-compatible instrument definitions + (C) raina 2005 + * Space Debris — MOD with tons of effects + (C) Captain/Image 1991 + * Changing Waves — for Funk Repeat emulation + (C) 4mat/orb 2023 + * Aboriginal Derivatives — for demonstrating Monotone compatibility. + (C) Jakim 2010 + * SWINGIN1 (rename to Swinging Waste) — for demonstrating Monotone compatibility. + (C) Phoenix/Hornet 2015 Play Data: play data are series of tracker-like instructions, visualised as: diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index ad37cb6..64570b3 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -3613,16 +3613,25 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region. // Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact. + // Mask is sized for the loop length at allocation time; if the loop bounds change + // (e.g. a new song reuses this instrument slot with different sample data) the old + // mask is stale and must be discarded — otherwise indexing past its end crashes the + // render thread with ArrayIndexOutOfBoundsException. var funkMask: ByteArray? = null fun toggleFunkBit(loopOffset: Int) { val len = (sampleLoopEnd - sampleLoopStart).coerceAtLeast(1) - val mask = funkMask ?: ByteArray((len + 7) / 8).also { funkMask = it } + val expectedSize = (len + 7) / 8 + var mask = funkMask + if (mask == null || mask.size != expectedSize) { + mask = ByteArray(expectedSize).also { funkMask = it } + } val idx = loopOffset.coerceIn(0, len - 1) mask[idx / 8] = (mask[idx / 8].toInt() xor (1 shl (idx and 7))).toByte() } fun funkBit(loopOffset: Int): Boolean { val mask = funkMask ?: return false val len = (sampleLoopEnd - sampleLoopStart).coerceAtLeast(1) + if (mask.size != (len + 7) / 8) { funkMask = null; return false } val idx = loopOffset.coerceIn(0, len - 1) return (mask[idx / 8].toInt() ushr (idx and 7)) and 1 != 0 }