Compare commits

...

5 Commits

Author SHA1 Message Date
minjaesong
e3bd4a1b59 more tets 2026-05-14 16:46:57 +09:00
minjaesong
70d953a784 LibTaud: off-by-one error on BPM parsing 2026-05-14 15:27:13 +09:00
minjaesong
f3ece28a10 taud: pattern ditto eff 2026-05-14 01:07:40 +09:00
minjaesong
3ecf842ac0 taut.js: Bohlen-Pierce tuning 2026-05-14 00:40:36 +09:00
minjaesong
6004060344 taut.js: 16-TET preset 2026-05-13 23:05:11 +09:00
8 changed files with 349 additions and 96 deletions

View File

@@ -735,6 +735,87 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr
--- ---
## 7 $xxyy — Pattern Ditto
**Plain.** A per-channel "fill the rest from above" marker: the engine copies the **$xx rows immediately preceding this cell on the same channel** and pastes them $yy times starting on this row. The destination block therefore covers `$xx × $yy` rows beginning at the ditto row inclusive. Any field (note, instrument, vol-column, pan-column, effect) that the composer has explicitly written into a destination row stays put and patches the corresponding field of the copied source cell — empty fields fall through to the source. The ditto opcode itself is consumed by the marker on its arming row; the rest of that row's columns are patched from the source as usual, so an empty arming row plays back identically to the first row of the source block.
For example, with `7 $1003` on row 16, rows 16..63 replay the contents of rows 0..15 three times. A `D $0400` punched onto row 22 simply overrides the effect column on that destination row; its note/vol/pan still come from the source row 6 (since (22 16) mod 16 = 6, and 0 + 6 = source row 6).
Boundary rules:
- The block stops at the end of the pattern: a ditto whose nominal span would overflow the pattern's row count clips silently at the final row.
- `$xx = $00`, `$yy = $00`, and any `$xx` greater than the row index on which the ditto sits are all treated as no-ops — there is nothing valid to copy from.
- A `7` cell appearing inside a source block is **not** recursively expanded: when that source row is pasted into a destination, its effect column is treated as empty. This keeps expansion single-pass and prevents unbounded nesting.
- Flow-control effects (B, C, S$Bx, S$Ex) that fall inside a source block still fire when their copy lands on a destination row, since the engine sees them as ordinary effect cells after expansion. Composers and converters should avoid placing S$Bx loop bounds wholly inside a ditto'd range — the loop counter is per-voice and the same destination row would be revisited twice with the same state.
**Compatibility.** Unique to Taud — no ST3/IT/PT equivalent. The effect has no memory.
**Implementation.** Per-voice state, all reset on pattern change alongside the existing pattern-loop / fine-pattern-delay clears:
- `dittoActive: bool`
- `dittoSourceStart: int` — first row of the source block (inclusive)
- `dittoLength: int` — $xx, the block size
- `dittoEndRow: int` — last destination row (inclusive)
At the very top of `applyTrackerRow`, before the per-voice reset of row-scope state, build an effective cell view for each voice:
```
raw = patternRows[V.pattern][N] # stored cell on row N for voice V
isArmer = (raw.effect == 0x7 and raw.effectArg != 0)
if isArmer:
length = (raw.effectArg >> 8) & 0xFF
repeats = raw.effectArg & 0xFF
if length > 0 and repeats > 0 and length <= N:
V.dittoSourceStart = N - length
V.dittoLength = length
V.dittoEndRow = min(N + length * repeats - 1, patternLength - 1)
V.dittoActive = true
# else: malformed argument — fall through with dittoActive unchanged
armRow = V.dittoSourceStart + V.dittoLength # always equals the row that armed this ditto
if V.dittoActive and armRow <= N <= V.dittoEndRow:
srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength)
src = patternRows[V.pattern][srcRow]
cell.note = (raw.note != 0xFFFF) ? raw.note : src.note
cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument
# SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns;
# any other (selector, value) pair is a write and patches the source.
cell.vol, cell.volEff = (raw.volEff, raw.vol) != (SEL_FINE, 0)
? (raw.vol, raw.volEff)
: (src.vol, src.volEff)
cell.pan, cell.panEff = (raw.panEff, raw.pan) != (SEL_FINE, 0)
? (raw.pan, raw.panEff)
: (src.pan, src.panEff)
# On the armer row, the 7-opcode is consumed by the marker, so for effect-column
# patching purposes the destination is treated as empty. Source 7-opcodes never
# propagate (no recursive expansion).
destOp, destArg = isArmer ? (0, 0) : (raw.effect, raw.effectArg)
if destOp != 0:
cell.effect, cell.effectArg = destOp, destArg
elif src.effect != 0x7:
cell.effect, cell.effectArg = src.effect, src.effectArg
else:
cell.effect, cell.effectArg = 0, 0
else:
cell = raw
```
The four ditto fields are not cleared at the natural end of the destination range; they simply stop matching the gating condition once `N` advances past `dittoEndRow`, and a later armer cell in the same pattern overwrites them in place. Explicit clears happen only on cue advance (B / C / natural pattern end) and full playhead reset, alongside the existing pattern-loop counters in `resetPatternLoopState` / `resetParams`.
The rest of `applyTrackerRow` then dispatches on `cell` exactly as for an undittoed row — note triggering, vol/pan column application, and effect handling are unchanged. The expansion mutates the in-memory cell view only; the stored pattern data is never rewritten.
Pattern-delay (S$Ex) re-runs `applyTrackerRow` on the same `N` — the ditto bookkeeping is idempotent across those re-entries because `dittoActive`, `dittoSourceStart`, `dittoLength`, and `dittoEndRow` already encode the destination range, and the armer guard `length <= N` makes repeated arming on the same row a no-op (the new state is identical to the old). The `armRow <= N` half of the gating condition is what protects against an S$Bx pattern-loop that jumps back to a row sitting strictly before the armer: rather than synthesising from a phantom source slot, the engine falls through to the raw cell.
Effect dispatch sees the synthesised effect, never the literal `7` opcode of the armer cell — `OP_7` therefore exists in the engine's opcode table only as an explicit no-op for the rare malformed-armer fallthrough (`length == 0`, `repeats == 0`, or `length > N`).
---
## 8 $xyzz — Bitcrusher ## 8 $xyzz — Bitcrusher
**Plain.** Applies a bitcrusher to the current voice. The crusher has two independent stages — a sample-rate reducer (`zz`, sample-and-hold) and a bit-depth quantiser (`y`) — and shares its clipping mode (`x`) with effect 9 (Overdrive). The two stages are orthogonal: enabling either is sufficient to engage the effect, and either can be active alone. **Plain.** Applies a bitcrusher to the current voice. The crusher has two independent stages — a sample-rate reducer (`zz`, sample-and-hold) and a bit-depth quantiser (`y`) — and shares its clipping mode (`x`) with effect 9 (Overdrive). The two stages are orthogonal: enabling either is sufficient to engage the effect, and either can be active alone.

View File

@@ -102,7 +102,7 @@ const fxNames = {
'4':"UNIMPLEMENTED", '4':"UNIMPLEMENTED",
'5':"UNIMPLEMENTED", '5':"UNIMPLEMENTED",
'6':"UNIMPLEMENTED", '6':"UNIMPLEMENTED",
'7':"UNIMPLEMENTED", '7':"Pattern Ditto",
'8':"Bitcrusher ", '8':"Bitcrusher ",
'9':"Overdrive ", '9':"Overdrive ",
A:"Tick speed ", A:"Tick speed ",
@@ -169,48 +169,92 @@ const volFxNames = {
const pitchTablePresets = { const pitchTablePresets = {
// index: pitch table number to be recorded on .taudproj file // index: pitch table number to be recorded on .taudproj file
0:{index:0,name:"null", table:[], sym:[]}, // when null is specified, hex numbers will be displayed instead 0:{index:0,name:"Raw format",table:[],interval:0x1000,sym:[]}, // when null is specified, hex numbers will be displayed instead
/* Xenharmonic, equal temperament */ /* Xenharmonic, equal temperament */
50:{index:50,name:"5-TET", table:[0x0,0x333,0x666,0x99A,0xCCD], 10:{index:10,name:"Octave only", table:[0x0],interval:0x1000,
sym:[`C${sym.accnull}`]},
20:{index:20,name:"2-TET", table:[0x0,0x800],interval:0x1000,
sym:[`C${sym.accnull}`,`F${sym.sharp}`]},
30:{index:30,name:"3-TET", table:[0x0,0x555,0xAAB],interval:0x1000,
sym:[`C${sym.accnull}`,`E${sym.accnull}`,`G${sym.sharp}`]},
40:{index:40,name:"4-TET", table:[0x0,0x400,0x800,0xC00],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.sharp}`,`F${sym.sharp}`,`A${sym.accnull}`]},
50:{index:50,name:"5-TET", table:[0x0,0x333,0x666,0x99A,0xCCD],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]},
70:{index:70,name:"7-TET", table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7], 60:{index:60,name:"6-TET", table:[0x0,0x2AB,0x555,0x800,0xAAB,0xD55],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.sharp}`]},
70:{index:70,name:"7-TET", table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]},
100:{index:100,name:"10-TET", table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66], 80:{index:80,name:"8-TET", table:[0x0,0x200,0x400,0x600,0x800,0xA00,0xC00,0xE00],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.accnull}`]},
90:{index:90,name:"9-TET", table:[0x0,0x1C7,0x38E,0x555,0x71C,0x8E4,0xAAB,0xC72,0xE39],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`,`B${sym.sharp}`]},
100:{index:100,name:"10-TET", table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.flat}`,`D${sym.accnull}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`]}, sym:[`C${sym.accnull}`,`D${sym.flat}`,`D${sym.accnull}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`]},
150:{index:150,name:"15-TET", table:[0x0,0x111,0x222,0x333,0x444,0x555,0x666,0x777,0x889,0x99A,0xAAB,0xBBC,0xCCD,0xDDE,0xEEF], 150:{index:150,name:"15-TET", table:[0x0,0x111,0x222,0x333,0x444,0x555,0x666,0x777,0x889,0x99A,0xAAB,0xBBC,0xCCD,0xDDE,0xEEF],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`]},
170:{index:170,name:"17-TET", table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F], 160:{index:160,name:"16-TET", table:[0x0,0x100,0x200,0x300,0x400,0x500,0x600,0x700,0x800,0x900,0xA00,0xB00,0xC00,0xD00,0xE00,0xF00],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.flat}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`,`B${sym.sharp}`,`C${sym.flat}`]},
170:{index:170,name:"17-TET", table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F],interval:0x1000,
sym:[`C${sym.accnull}`,`D${sym.flat}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.flat}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.flat}`,`F${sym.sharp}`,`G${sym.accnull}`,`A${sym.flat}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.flat}`,`A${sym.sharp}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.flat}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.flat}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.flat}`,`F${sym.sharp}`,`G${sym.accnull}`,`A${sym.flat}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.flat}`,`A${sym.sharp}`,`B${sym.accnull}`]},
190:{index:190,name:"19-TET", table:[0x0,0xD8,0x1AF,0x287,0x35E,0x436,0x50D,0x5E5,0x6BD,0x794,0x86C,0x943,0xA1B,0xAF3,0xBCA,0xCA2,0xD79,0xE51,0xF28], 190:{index:190,name:"19-TET", table:[0x0,0xD8,0x1AF,0x287,0x35E,0x436,0x50D,0x5E5,0x6BD,0x794,0x86C,0x943,0xA1B,0xAF3,0xBCA,0xCA2,0xD79,0xE51,0xF28],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`,`B${sym.sharp}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`,`B${sym.sharp}`]},
220:{index:220,name:"22-TET", table:[0x0,0xBA,0x174,0x22F,0x2E9,0x3A3,0x45D,0x517,0x5D1,0x68C,0x746,0x800,0x8BA,0x974,0xA2F,0xAE9,0xBA3,0xC5D,0xD17,0xDD1,0xE8C,0xF46], 220:{index:220,name:"22-TET", table:[0x0,0xBA,0x174,0x22F,0x2E9,0x3A3,0x45D,0x517,0x5D1,0x68C,0x746,0x800,0x8BA,0x974,0xA2F,0xAE9,0xBA3,0xC5D,0xD17,0xDD1,0xE8C,0xF46],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`]},
240:{index:240,name:"24-TET", table:[0x0,0xAB,0x155,0x200,0x2AB,0x355,0x400,0x4AB,0x555,0x600,0x6AB,0x755,0x800,0x8AB,0x955,0xA00,0xAAB,0xB55,0xC00,0xCAB,0xD55,0xE00,0xEAB,0xF55], 240:{index:240,name:"24-TET", table:[0x0,0xAB,0x155,0x200,0x2AB,0x355,0x400,0x4AB,0x555,0x600,0x6AB,0x755,0x800,0x8AB,0x955,0xA00,0xAAB,0xB55,0xC00,0xCAB,0xD55,0xE00,0xEAB,0xF55],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`]}, sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`]},
310:{index:310,name:"31-TET", table:[0x0,0x84,0x108,0x18C,0x211,0x295,0x319,0x39D,0x421,0x4A5,0x529,0x5AD,0x632,0x6B6,0x73A,0x7BE,0x842,0x8C6,0x94A,0x9CE,0xA53,0xAD7,0xB5B,0xBDF,0xC63,0xCE7,0xD6B,0xDEF,0xE74,0xEF8,0xF7C], 310:{index:310,name:"31-TET", table:[0x0,0x84,0x108,0x18C,0x211,0x295,0x319,0x39D,0x421,0x4A5,0x529,0x5AD,0x632,0x6B6,0x73A,0x7BE,0x842,0x8C6,0x94A,0x9CE,0xA53,0xAD7,0xB5B,0xBDF,0xC63,0xCE7,0xD6B,0xDEF,0xE74,0xEF8,0xF7C],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.demiflat}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`,`C${sym.demiflat}`]}, sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.demiflat}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`,`C${sym.demiflat}`]},
410:{index:410,name:"41-TET", table:[0x0,0x64,0xC8,0x12C,0x190,0x1F4,0x257,0x2BB,0x31F,0x383,0x3E7,0x44B,0x4AF,0x513,0x577,0x5DB,0x63E,0x6A2,0x706,0x76A,0x7CE,0x832,0x896,0x8FA,0x95E,0x9C2,0xA25,0xA89,0xAED,0xB51,0xBB5,0xC19,0xC7D,0xCE1,0xD45,0xDA9,0xE0C,0xE70,0xED4,0xF38,0xF9C], 410:{index:410,name:"41-TET (Kite)", table:[0x0,0x64,0xC8,0x12C,0x190,0x1F4,0x257,0x2BB,0x31F,0x383,0x3E7,0x44B,0x4AF,0x513,0x577,0x5DB,0x63E,0x6A2,0x706,0x76A,0x7CE,0x832,0x896,0x8FA,0x95E,0x9C2,0xA25,0xA89,0xAED,0xB51,0xBB5,0xC19,0xC7D,0xCE1,0xD45,0xDA9,0xE0C,0xE70,0xED4,0xF38,0xF9C],interval:0x1000,
sym:[`-C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`-C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`-D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`-D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`-E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`-F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`-F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`-G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`-G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`-A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`-A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`-B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]}, sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]},
530:{index:530,name:"53-TET (Kite notation)", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3], 530:{index:530,name:"53-TET (Kite)", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],interval:0x1000,
sym:[`-C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`-C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`-D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`-D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`-E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`-F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`-F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`-G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`-G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`-A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`-A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`-B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]}, sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]},
531:{index:531,name:"53-TET (Pythagorean Notation)", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3], 531:{index:531,name:"53-TET (Pythagorean)", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],interval:0x1000,
sym:[`C${sym.accnull}`,`B${sym.sharp}`,`A${sym.triplesharp}`,`E${sym.tripleflat}`,`D${sym.flat}`,`C${sym.sharp}`,`B${sym.doublesharp}`,`F${sym.tripleflat}`,`E${sym.doubleflat}`,`D${sym.accnull}`,`C${sym.doublesharp}`,`B${sym.triplesharp}`,`F${sym.doubleflat}`,`E${sym.flat}`,`D${sym.sharp}`,`C${sym.triplesharp}`,`G${sym.tripleflat}`,`F${sym.flat}`,`E${sym.accnull}`,`D${sym.doublesharp}`,`C${sym.quadsharp}`,`G${sym.doubleflat}`,`F${sym.accnull}`,`E${sym.sharp}`,`D${sym.triplesharp}`,`A${sym.tripleflat}`,`G${sym.flat}`,`F${sym.sharp}`,`E${sym.doublesharp}`,`D${sym.quadsharp}`,`A${sym.doubleflat}`,`G${sym.accnull}`,`F${sym.doublesharp}`,`E${sym.triplesharp}`,`B${sym.tripleflat}`,`A${sym.flat}`,`G${sym.sharp}`,`F${sym.triplesharp}`,`C${sym.tripleflat}`,`B${sym.doubleflat}`,`A${sym.accnull}`,`G${sym.doublesharp}`,`F${sym.quadsharp}`,`C${sym.doubleflat}`,`B${sym.flat}`,`A${sym.sharp}`,`G${sym.triplesharp}`,`D${sym.tripleflat}`,`C${sym.flat}`,`B${sym.accnull}`,`A${sym.doublesharp}`,`G${sym.quadsharp}`,`D${sym.doubleflat}`]}, sym:[`C${sym.accnull}`,`B${sym.sharp}`,`A${sym.triplesharp}`,`E${sym.tripleflat}`,`D${sym.flat}`,`C${sym.sharp}`,`B${sym.doublesharp}`,`F${sym.tripleflat}`,`E${sym.doubleflat}`,`D${sym.accnull}`,`C${sym.doublesharp}`,`B${sym.triplesharp}`,`F${sym.doubleflat}`,`E${sym.flat}`,`D${sym.sharp}`,`C${sym.triplesharp}`,`G${sym.tripleflat}`,`F${sym.flat}`,`E${sym.accnull}`,`D${sym.doublesharp}`,`C${sym.quadsharp}`,`G${sym.doubleflat}`,`F${sym.accnull}`,`E${sym.sharp}`,`D${sym.triplesharp}`,`A${sym.tripleflat}`,`G${sym.flat}`,`F${sym.sharp}`,`E${sym.doublesharp}`,`D${sym.quadsharp}`,`A${sym.doubleflat}`,`G${sym.accnull}`,`F${sym.doublesharp}`,`E${sym.triplesharp}`,`B${sym.tripleflat}`,`A${sym.flat}`,`G${sym.sharp}`,`F${sym.triplesharp}`,`C${sym.tripleflat}`,`B${sym.doubleflat}`,`A${sym.accnull}`,`G${sym.doublesharp}`,`F${sym.quadsharp}`,`C${sym.doubleflat}`,`B${sym.flat}`,`A${sym.sharp}`,`G${sym.triplesharp}`,`D${sym.tripleflat}`,`C${sym.flat}`,`B${sym.accnull}`,`A${sym.doublesharp}`,`G${sym.quadsharp}`,`D${sym.doubleflat}`]},
960:{index:960,name:"96-TET", table:[0x0,0x2B,0x55,0x80,0xAB,0xD5,0x100,0x12B,0x155,0x180,0x1AB,0x1D5,0x200,0x22B,0x255,0x280,0x2AB,0x2D5,0x300,0x32B,0x355,0x380,0x3AB,0x3D5,0x400,0x42B,0x455,0x480,0x4AB,0x4D5,0x500,0x52B,0x555,0x580,0x5AB,0x5D5,0x600,0x62B,0x655,0x680,0x6AB,0x6D5,0x700,0x72B,0x755,0x780,0x7AB,0x7D5,0x800,0x82B,0x855,0x880,0x8AB,0x8D5,0x900,0x92B,0x955,0x980,0x9AB,0x9D5,0xA00,0xA2B,0xA55,0xA80,0xAAB,0xAD5,0xB00,0xB2B,0xB55,0xB80,0xBAB,0xBD5,0xC00,0xC2B,0xC55,0xC80,0xCAB,0xCD5,0xD00,0xD2B,0xD55,0xD80,0xDAB,0xDD5,0xE00,0xE2B,0xE55,0xE80,0xEAB,0xED5,0xF00,0xF2B,0xF55,0xF80,0xFAB,0xFD5], 960:{index:960,name:"96-TET (Kite)", table:[0x0,0x2B,0x55,0x80,0xAB,0xD5,0x100,0x12B,0x155,0x180,0x1AB,0x1D5,0x200,0x22B,0x255,0x280,0x2AB,0x2D5,0x300,0x32B,0x355,0x380,0x3AB,0x3D5,0x400,0x42B,0x455,0x480,0x4AB,0x4D5,0x500,0x52B,0x555,0x580,0x5AB,0x5D5,0x600,0x62B,0x655,0x680,0x6AB,0x6D5,0x700,0x72B,0x755,0x780,0x7AB,0x7D5,0x800,0x82B,0x855,0x880,0x8AB,0x8D5,0x900,0x92B,0x955,0x980,0x9AB,0x9D5,0xA00,0xA2B,0xA55,0xA80,0xAAB,0xAD5,0xB00,0xB2B,0xB55,0xB80,0xBAB,0xBD5,0xC00,0xC2B,0xC55,0xC80,0xCAB,0xCD5,0xD00,0xD2B,0xD55,0xD80,0xDAB,0xDD5,0xE00,0xE2B,0xE55,0xE80,0xEAB,0xED5,0xF00,0xF2B,0xF55,0xF80,0xFAB,0xFD5],interval:0x1000,
sym:[`-C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`-C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`-C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`-D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`-D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`-D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`-D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`-E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`-E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`-E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`-F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`-F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`-F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`-G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`-G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`-G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`-G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`-A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`-A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`-A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`-A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`-B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`-B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`-B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`]}, sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`${BIGDOT}C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`${BIGDOT}D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`${BIGDOT}D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`${BIGDOT}E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`${BIGDOT}E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`${BIGDOT}F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`${BIGDOT}G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`${BIGDOT}G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`${BIGDOT}A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`${BIGDOT}A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`${BIGDOT}B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`${BIGDOT}B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`,`${sym.dntick}C-`]},
/* 12-TET variations */ /* 12-TET variations */
120:{index:120,name:"12-TET", table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB], 120:{index:120,name:"12-TET", table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10121:{index:10121,name:"Pythagorean Diminished Fifth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC], 10121:{index:10121,name:"Pythagorean dim. 5th", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10122:{index:10122,name:"Pythagorean Augmented Fourth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC], 10122:{index:10122,name:"Pythagorean aug. 4th", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10123:{index:10123,name:"\u00FC\u00FD\u00FE", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC], 10123:{index:10123,name:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000,
sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]}, sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]},
/* non-octave */
35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce", table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C,
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
} }
// check pitchTablePresets integrity
function checkPitchTablePresetsIntegrity() {
const seenIndices = {}
for (const key in pitchTablePresets) {
const preset = pitchTablePresets[key]
const keyNum = +key
if (preset == null) throw Error(`pitchTablePresets[${key}] is null/undefined`)
if (typeof preset.index !== 'number') throw Error(`pitchTablePresets[${key}].index is not a number`)
if (preset.index !== keyNum) throw Error(`pitchTablePresets[${key}].index (${preset.index}) does not match its key (${key})`)
if (seenIndices[preset.index]) throw Error(`duplicate index ${preset.index} in pitchTablePresets`)
seenIndices[preset.index] = true
if (typeof preset.name !== 'string') throw Error(`pitchTablePresets[${key}].name is not a string`)
if (!Array.isArray(preset.table)) throw Error(`pitchTablePresets[${key}].table is not an array`)
if (!Array.isArray(preset.sym)) throw Error(`pitchTablePresets[${key}].sym is not an array`)
if (preset.table.length !== preset.sym.length) throw Error(`pitchTablePresets[${key}] (${preset.name}): table.length (${preset.table.length}) != sym.length (${preset.sym.length})`)
for (let i = 0; i < preset.table.length; i++) {
const v = preset.table[i]
if (typeof v !== 'number' || !Number.isFinite(v)) throw Error(`pitchTablePresets[${key}] (${preset.name}): table[${i}] is not a finite number`)
if (i > 0 && v <= preset.table[i - 1]) throw Error(`pitchTablePresets[${key}] (${preset.name}): table is not strictly ascending at index ${i} (0x${preset.table[i-1].toString(16)} -> 0x${v.toString(16)})`)
}
for (let i = 0; i < preset.sym.length; i++) {
if (typeof preset.sym[i] !== 'string') throw Error(`pitchTablePresets[${key}] (${preset.name}): sym[${i}] is not a string`)
}
}
}
checkPitchTablePresetsIntegrity()
const volEffSym = [sym.volset, sym.volup, sym.voldn, sym.volfineup, sym.volfinedn] const volEffSym = [sym.volset, sym.volup, sym.voldn, sym.volfineup, sym.volfinedn]
const panEffSym = [sym.panset, sym.panri, sym.panle, sym.panfineri, sym.panfinele] const panEffSym = [sym.panset, sym.panri, sym.panle, sym.panfineri, sym.panfinele]
@@ -229,24 +273,46 @@ let beatDivSecondary = 16
let hasUnsavedChanges = false let hasUnsavedChanges = false
let patternsOutOfSync = false // in-memory song.patterns has edits not yet pushed to the audio adapter let patternsOutOfSync = false // in-memory song.patterns has edits not yet pushed to the audio adapter
// pitchSymLut[pitchInOct] = [symString, octaveOffset] // Pitch encoding: a 16-bit absolute value with Middle C anchored at 0x5000.
// octaveOffset is 1 when pitchInOct is closer to the next octave's root (wraps up) than to any table entry. // For octave systems (interval == 0x1000) the value decomposes naturally as
// Call rebuildPitchLut() whenever PITCH_PRESET_IDX changes. // (octave << 12) | pitchInOctave. For non-octave systems the "period" (e.g.
const pitchSymLut = new Array(0x1000) // the BP tritave at 0x195C) does not align with 4-bit boundaries; the period
// index and offset must be computed by integer-divmod against the interval,
// using ANCHOR_NOTE / ANCHOR_PERIOD as the fixed reference point.
const ANCHOR_NOTE = 0x5000
const ANCHOR_PERIOD = 5
function decomposeNote(note, interval) {
const delta = note - ANCHOR_NOTE
const k = Math.floor(delta / interval)
return [ANCHOR_PERIOD + k, delta - k * interval]
}
function composeNote(periodIdx, offset, interval) {
return ANCHOR_NOTE + (periodIdx - ANCHOR_PERIOD) * interval + offset
}
// pitchSymLut[offsetInPeriod] = [symString, periodOffset]
// periodOffset is 1 when offsetInPeriod is closer to the next period's root
// (one `interval` above) than to any table entry — i.e. the note should wrap
// up to the first entry of the next period.
// Call rebuildPitchLut() whenever PITCH_PRESET_IDX changes; the LUT is sized
// to the preset's interval so non-octave tunings (e.g. BP at 0x195C) work.
let pitchSymLut = new Array(0x1000)
function rebuildPitchLut() { function rebuildPitchLut() {
const preset = pitchTablePresets[PITCH_PRESET_IDX] const preset = pitchTablePresets[PITCH_PRESET_IDX]
if (!preset || preset.table.length === 0) return if (!preset || preset.table.length === 0) return
const table = preset.table const table = preset.table
const syms = preset.sym const syms = preset.sym
for (let p = 0; p < 0x1000; p++) { const interval = preset.interval
let best = 0, bestDist = 0x1000 if (pitchSymLut.length !== interval) pitchSymLut = new Array(interval)
for (let p = 0; p < interval; p++) {
let best = 0, bestDist = interval
for (let i = 0; i < table.length; i++) { for (let i = 0; i < table.length; i++) {
const d = Math.abs(p - table[i]) const d = Math.abs(p - table[i])
if (d < bestDist) { bestDist = d; best = i } if (d < bestDist) { bestDist = d; best = i }
} }
// Distance to the next octave's root (0x1000) vs nearest table entry. // Distance to the next period's root (one interval up) vs nearest table entry.
if ((0x1000 - p) < bestDist) { if ((interval - p) < bestDist) {
pitchSymLut[p] = [syms[0], 1] pitchSymLut[p] = [syms[0], 1]
} else { } else {
pitchSymLut[p] = [syms[best], 0] pitchSymLut[p] = [syms[best], 0]
@@ -275,17 +341,18 @@ rebuildPitchLut()
// real musical fact that it's at fifth-circle distance 5 from tonic and // real musical fact that it's at fifth-circle distance 5 from tonic and
// hence highly tense (cf. Krumhansl's tonal hierarchy: B is the least // hence highly tense (cf. Krumhansl's tonal hierarchy: B is the least
// stable diatonic note in C, despite sitting a semitone below C). // stable diatonic note in C, despite sitting a semitone below C).
function _cadTension(p, tonic) { function _cadTension(p, tonic, interval) {
const FIFTH_PC = 0x95A const FIFTH_PC = 0x95A
const TONIC_TOL = 0x40 const TONIC_TOL = 0x40
const d = ((p - tonic) % 0x1000 + 0x1000) % 0x1000 const half = interval >>> 1
const cyclic = (d <= 0x800) ? d : (0x1000 - d) const d = ((p - tonic) % interval + interval) % interval
const cyclic = (d <= half) ? d : (interval - d)
let bestT = (cyclic <= TONIC_TOL) ? cyclic : Infinity let bestT = (cyclic <= TONIC_TOL) ? cyclic : Infinity
for (let k = -6; k <= 6; k++) { for (let k = -6; k <= 6; k++) {
if (k === 0) continue if (k === 0) continue
const target = ((k * FIFTH_PC) % 0x1000 + 0x1000) % 0x1000 const target = ((k * FIFTH_PC) % interval + interval) % interval
let dist = Math.abs(d - target) let dist = Math.abs(d - target)
if (dist > 0x800) dist = 0x1000 - dist if (dist > half) dist = interval - dist
const candT = Math.abs(k) * 0x100 + dist const candT = Math.abs(k) * 0x100 + dist
if (candT < bestT) bestT = candT if (candT < bestT) bestT = candT
} }
@@ -308,13 +375,14 @@ const _HARM_REFS = [
[0xBCB, 3.0], // 5:3 major sixth [0xBCB, 3.0], // 5:3 major sixth
[0xD3D, 4.0], // 9:5 minor seventh [0xD3D, 4.0], // 9:5 minor seventh
] ]
function _harmonicCost(p, tonic) { function _harmonicCost(p, tonic, interval) {
const d = ((p - tonic) % 0x1000 + 0x1000) % 0x1000 const half = interval >>> 1
const d = ((p - tonic) % interval + interval) % interval
let best = Infinity let best = Infinity
for (let i = 0; i < _HARM_REFS.length; i++) { for (let i = 0; i < _HARM_REFS.length; i++) {
const ref = _HARM_REFS[i] const ref = _HARM_REFS[i]
let dist = Math.abs(d - ref[0]) let dist = Math.abs(d - ref[0])
if (dist > 0x800) dist = 0x1000 - dist if (dist > half) dist = interval - dist
const cost = ref[1] * dist const cost = ref[1] * dist
if (cost < best) best = cost if (cost < best) best = cost
} }
@@ -358,10 +426,34 @@ function _harmonicCost(p, tonic) {
// onto the JI attractor field — "precision during landing". // onto the JI attractor field — "precision during landing".
function retuneAllPatterns(newIdx, method) { function retuneAllPatterns(newIdx, method) {
if (method !== 'delta' && method !== 'cadence' && method !== 'harmonic') method = 'pitch' if (method !== 'delta' && method !== 'cadence' && method !== 'harmonic') method = 'pitch'
const preset = pitchTablePresets[newIdx] const newPreset = pitchTablePresets[newIdx]
if (!preset) return if (!newPreset) return
const table = preset.table const srcPreset = pitchTablePresets[PITCH_PRESET_IDX]
if (table.length > 0) { const newTable = newPreset.table
const newInterval = newPreset.interval
// Tension/harmonic shapes are read out of the SOURCE tuning's modular
// space — they describe the composition the user wrote, not the snap
// grid we're mapping onto. For octave→octave retunes this collapses to
// the original behaviour (both intervals are 0x1000).
const srcInterval = srcPreset.interval || 0x1000
// Yield candidate absolute pitches in the new tuning whose period root
// lies within ±1 period of `absRef`. Includes the next period's root
// itself so a target that lands just past the top entry can snap up.
const forEachCandidate = (absRef, fn) => {
const baseK = Math.floor((absRef - ANCHOR_NOTE) / newInterval)
for (let dK = -1; dK <= 1; dK++) {
const root = ANCHOR_NOTE + (baseK + dK) * newInterval
for (let i = 0; i < newTable.length; i++) {
const cand = root + newTable[i]
if (cand >= 0 && cand <= 0xFFFF) fn(cand)
}
const nextRoot = root + newInterval
if (nextRoot >= 0 && nextRoot <= 0xFFFF) fn(nextRoot)
}
}
if (newTable.length > 0) {
for (let p = 0; p < song.numPats; p++) { for (let p = 0; p < song.numPats; p++) {
const ptn = song.patterns[p] const ptn = song.patterns[p]
let prevOrigAbs = -1 let prevOrigAbs = -1
@@ -372,7 +464,9 @@ function retuneAllPatterns(newIdx, method) {
const off = 8 * row const off = 8 * row
const note = ptn[off] | (ptn[off+1] << 8) const note = ptn[off] | (ptn[off+1] << 8)
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue
tonic = note & 0xFFF // Use the full absolute pitch as tonic; the modular ops
// in _cadTension / _harmonicCost normalise it.
tonic = note
break break
} }
} }
@@ -380,17 +474,14 @@ function retuneAllPatterns(newIdx, method) {
const off = 8 * row const off = 8 * row
const note = ptn[off] | (ptn[off+1] << 8) const note = ptn[off] | (ptn[off+1] << 8)
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue
let octave = (note >>> 12) & 0xF const origAbs = note
const pitch = note & 0xFFF
const origAbs = (octave << 12) | pitch
let newAbs let newAbs
if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) { if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) {
const targetAbs = prevMappedAbs + (origAbs - prevOrigAbs) const targetAbs = prevMappedAbs + (origAbs - prevOrigAbs)
const baseOc = (targetAbs >> 12)
let targetDeltaT = 0, tMappedPrev = 0, lambda = 0 let targetDeltaT = 0, tMappedPrev = 0, lambda = 0
if (method === 'cadence') { if (method === 'cadence') {
targetDeltaT = _cadTension(origAbs, tonic) - _cadTension(prevOrigAbs, tonic) targetDeltaT = _cadTension(origAbs, tonic, srcInterval) - _cadTension(prevOrigAbs, tonic, srcInterval)
tMappedPrev = _cadTension(prevMappedAbs, tonic) tMappedPrev = _cadTension(prevMappedAbs, tonic, srcInterval)
} else if (method === 'harmonic') { } else if (method === 'harmonic') {
let duration = 1 let duration = 1
for (let r = row + 1; r < ROWS_PER_PAT; r++) { for (let r = row + 1; r < ROWS_PER_PAT; r++) {
@@ -402,42 +493,27 @@ function retuneAllPatterns(newIdx, method) {
lambda = 1 - Math.exp(-(duration - 1) / 4) lambda = 1 - Math.exp(-(duration - 1) / 4)
} }
let bestAbs = 0, bestScore = Infinity let bestAbs = 0, bestScore = Infinity
const tryCand = (cand) => { forEachCandidate(targetAbs, (cand) => {
const pitchErr = Math.abs(cand - targetAbs) const pitchErr = Math.abs(cand - targetAbs)
let score = pitchErr let score = pitchErr
if (method === 'cadence') { if (method === 'cadence') {
const candDeltaT = _cadTension(cand, tonic) - tMappedPrev const candDeltaT = _cadTension(cand, tonic, srcInterval) - tMappedPrev
score = Math.abs(candDeltaT - targetDeltaT) * 2 + pitchErr score = Math.abs(candDeltaT - targetDeltaT) * 2 + pitchErr
} else if (method === 'harmonic') { } else if (method === 'harmonic') {
score = pitchErr + lambda * _harmonicCost(cand, tonic) score = pitchErr + lambda * _harmonicCost(cand, tonic, srcInterval)
} }
if (score < bestScore) { bestScore = score; bestAbs = cand } if (score < bestScore) { bestScore = score; bestAbs = cand }
} })
for (let dOc = -1; dOc <= 1; dOc++) {
const oc = baseOc + dOc
if (oc < 0 || oc > 0xF) continue
const ocAbs = oc << 12
for (let i = 0; i < table.length; i++) tryCand(ocAbs + table[i])
// Also consider the next octave's root (0x1000 above
// this octave's base) so an interval that lands just
// past the top entry can snap up to the octave.
if (oc < 0xF) tryCand(ocAbs + 0x1000)
}
newAbs = bestAbs newAbs = bestAbs
} else { } else {
let best = 0, bestDist = 0x1000 // Nearest-pitch: snap source absolute pitch to the closest
for (let i = 0; i < table.length; i++) { // entry in the new tuning's snap grid.
const d = Math.abs(pitch - table[i]) let bestAbs = 0, bestDist = Infinity
if (d < bestDist) { bestDist = d; best = i } forEachCandidate(origAbs, (cand) => {
} const d = Math.abs(cand - origAbs)
let newPitch, newOctave = octave if (d < bestDist) { bestDist = d; bestAbs = cand }
if ((0x1000 - pitch) < bestDist) { })
if (newOctave < 0xF) { newOctave += 1; newPitch = 0 } newAbs = bestAbs
else { newPitch = table[table.length - 1] }
} else {
newPitch = table[best]
}
newAbs = (newOctave << 12) | newPitch
} }
if (newAbs < 0) newAbs = 0 if (newAbs < 0) newAbs = 0
if (newAbs > 0xFFFF) newAbs = 0xFFFF if (newAbs > 0xFFFF) newAbs = 0xFFFF
@@ -482,9 +558,11 @@ function noteToStr(note) {
if (note === 0xFFFF) return sym.middot.repeat(4) if (note === 0xFFFF) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut if (note === 0xFFFE) return sym.notecut
if (note === 0x0000) return sym.keyoff if (note === 0x0000) return sym.keyoff
if (pitchTablePresets[PITCH_PRESET_IDX].table.length === 0) return note.hex04() const preset = pitchTablePresets[PITCH_PRESET_IDX]
const [s, o] = pitchSymLut[note & 0xFFF] if (preset.table.length === 0) return note.hex04()
return s + ((note >> 12) - 1 + o).toString(16) // octave 10 -> 'a' const [period, offset] = decomposeNote(note, preset.interval)
const [s, o] = pitchSymLut[offset]
return s + (period - 1 + o).toString(16) // period 10 -> 'a'
} }
/** /**
@@ -3318,10 +3396,10 @@ function openRetunePopup() {
const methodCycle = ['pitch', 'harmonic', 'delta'/*, 'cadence'*/] const methodCycle = ['pitch', 'harmonic', 'delta'/*, 'cadence'*/]
let method = 'pitch' let method = 'pitch'
const pw = 44 const pw = 42
const listH = Math.min(n, 12) const listH = Math.min(n, 15)
const ph = listH + 6 const ph = listH + 5
const px = ((SCRW - pw) / 2 | 0) + 1 const px = ((SCRW - pw) / 2 | 0)
const py = ((SCRH - ph) / 2 | 0) const py = ((SCRH - ph) / 2 | 0)
const listX = px + 2 const listX = px + 2
const listY = py + 3 const listY = py + 3
@@ -3340,7 +3418,7 @@ function openRetunePopup() {
popup.drawFrame() popup.drawFrame()
con.move(py + 1, px + 2) con.move(py + 1, px + 2)
con.color_pair(colWHITE, colPopupBack) con.color_pair(colStatus, colPopupBack)
print('Select new tuning preset:') print('Select new tuning preset:')
con.move(py + 2, px + 2) con.move(py + 2, px + 2)
@@ -3396,7 +3474,7 @@ function openRetunePopup() {
con.color_pair(colStatus, colPopupBack) con.color_pair(colStatus, colPopupBack)
print(`Method `) print(`Method `)
con.color_pair(colVoiceHdr, colPopupBack) con.color_pair(colVoiceHdr, colPopupBack)
print(`q `) print(`Q `)
con.color_pair(colStatus, colPopupBack) con.color_pair(colStatus, colPopupBack)
print(`Cancel`) print(`Cancel`)

Binary file not shown.

View File

@@ -118,7 +118,7 @@ function uploadTaudFile(inFile, songIndex, playhead) {
let patBinCompSize = _peekU32LE(filePtr, entryOff + 18) let patBinCompSize = _peekU32LE(filePtr, entryOff + 18)
let cueSheetCompSize = _peekU32LE(filePtr, entryOff + 22) let cueSheetCompSize = _peekU32LE(filePtr, entryOff + 22)
let bpm = bpmStored + 24 let bpm = bpmStored + 25
let patsToLoad = numPatsLo | (numPatsHi << 8) let patsToLoad = numPatsLo | (numPatsHi << 8)
// -- 6. Decompress + upload patterns -------------------------------------- // -- 6. Decompress + upload patterns --------------------------------------
@@ -210,7 +210,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 - 24) & 0xFF let bpmStored = (bpm - 25) & 0xFF
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
@@ -272,7 +272,7 @@ 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 24 bias bpmStored, // BPM with 25 bias
tickRate, // initial tick-rate tickRate, // initial 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)

View File

@@ -2726,9 +2726,10 @@ prefixes:
* Repetition of: * Repetition of:
Uint8 Notation index (starting from zero) used by songs Uint8 Notation index (starting from zero) used by songs
Uint32 Size of this notation following this field Uint32 Size of this notation following this field
Uint16 Reserved for flags Uint16 Reserved for flags
Float32 Interval size (octave system = 2.0f). If you are not using an interval system (which means you are responsible for defining every note expressible), this must be NaN. 0f and Infinity are considered illegal Uint16 Interval size in 4096-TET lattice (octave = 0x1000, tritave = 0x195C). If you are not using an interval system (which means you are responsible for defining every note expressible), this must be 0.
Uint16 Notes between interval MINUS ONE (or octave); 12-TET will have value 11 Uint16 Reserved for float32 interval size (should it be in 4096-TET which is inexact or frequency multiplier which is exact but difficult to implement?)
Uint16 Notes between interval (or octave) MINUS ONE; 12-TET will have value 11
Byte[8] Reserved Byte[8] Reserved
Byte[*] Name, null terminated. Encoding: UTF-8 Byte[*] Name, null terminated. Encoding: UTF-8
Byte[*] Notation table. 0xFF-separated and null-terminated. Encoding: Taud charset Byte[*] Notation table. 0xFF-separated and null-terminated. Encoding: Taud charset

View File

@@ -1279,6 +1279,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
private object EffectOp { private object EffectOp {
const val OP_NONE = 0x00 const val OP_NONE = 0x00
const val OP_1 = 0x01 const val OP_1 = 0x01
const val OP_7 = 0x07
const val OP_8 = 0x08 const val OP_8 = 0x08
const val OP_9 = 0x09 const val OP_9 = 0x09
const val OP_A = 0x0A const val OP_A = 0x0A
@@ -2224,9 +2225,69 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val patNum = cue.patterns[vi] val patNum = cue.patterns[vi]
if (patNum == 0xFFF) continue if (patNum == 0xFFF) continue
val patIdx = patNum.coerceIn(0, 4095) val patIdx = patNum.coerceIn(0, 4095)
val row = playdata[patIdx][ts.rowIndex] val rawRow = playdata[patIdx][ts.rowIndex]
val voice = ts.voices[vi] val voice = ts.voices[vi]
// ── Pattern Ditto (effect 7) row-time expansion ──
// See TAUD_NOTE_EFFECTS.md §7. Arm the destination range when this row
// carries a 7-opcode with a valid argument; then, if the current row
// sits inside an active destination block, synthesise an effective cell
// that combines the source-block cell with any explicit fields the
// composer punched into the destination row.
val n = ts.rowIndex
val isArmer = (rawRow.effect == EffectOp.OP_7 && rawRow.effectArg != 0)
if (isArmer) {
val length = (rawRow.effectArg ushr 8) and 0xFF
val repeats = rawRow.effectArg and 0xFF
if (length > 0 && repeats > 0 && length <= n) {
val patLen = (cue.instruction as? PlayInstPatLen)?.rows ?: 64
voice.dittoSourceStart = n - length
voice.dittoLength = length
voice.dittoEndRow = minOf(n + length * repeats - 1, patLen - 1)
voice.dittoActive = true
}
// else: malformed — leave any previously-armed ditto state alone.
}
val dittoArmRow = voice.dittoSourceStart + voice.dittoLength
val row: TaudPlayData =
if (voice.dittoActive && n in dittoArmRow..voice.dittoEndRow) {
val rel = (n - voice.dittoSourceStart) % voice.dittoLength
val srcRow = voice.dittoSourceStart + rel
val src = playdata[patIdx][srcRow]
// Vol- / pan-column "no-op" sentinel is SEL_FINE (3) with value 0.
val volIsSet = !(rawRow.volumeEff == 3 && rawRow.volume == 0)
val panIsSet = !(rawRow.panEff == 3 && rawRow.pan == 0)
// On the armer row, the 7-opcode is consumed by the marker, so
// for effect-column patching purposes the destination is treated
// as empty. Source 7-opcodes never propagate (no recursive
// expansion).
val destOp = if (isArmer) 0 else rawRow.effect
val destArg = if (isArmer) 0 else rawRow.effectArg
val effOp: Int
val effArg: Int
when {
destOp != 0 -> { effOp = destOp; effArg = destArg }
src.effect != EffectOp.OP_7 -> { effOp = src.effect; effArg = src.effectArg }
else -> { effOp = 0; effArg = 0 }
}
TaudPlayData(
note = if (rawRow.note != 0xFFFF) rawRow.note else src.note,
instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment,
volume = if (volIsSet) rawRow.volume else src.volume,
volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff,
pan = if (panIsSet) rawRow.pan else src.pan,
panEff = if (panIsSet) rawRow.panEff else src.panEff,
effect = effOp,
effectArg = effArg,
)
} else {
rawRow
}
// Reset per-row transient state. // Reset per-row transient state.
voice.cutAtTick = -1 voice.cutAtTick = -1
voice.noteDelayTick = -1 voice.noteDelayTick = -1
@@ -2347,6 +2408,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
private fun applyEffectRow(ts: TrackerState, playhead: Playhead, voice: Voice, vi: Int, op: Int, rawArg: Int) { private fun applyEffectRow(ts: TrackerState, playhead: Playhead, voice: Voice, vi: Int, op: Int, rawArg: Int) {
when (op) { when (op) {
EffectOp.OP_NONE -> {} EffectOp.OP_NONE -> {}
EffectOp.OP_7 -> {
// 7 $xxyy — Pattern Ditto. See TAUD_NOTE_EFFECTS.md §7.
// The opcode is a marker only; the row-time expansion in
// [applyTrackerRow] consumes the armer cell and substitutes the
// effective row from the source block, so by the time dispatch
// reaches here either (a) the cell was an armer and we already
// overwrote the synthesised row's effect to 0 / source effect,
// or (b) we hit a malformed 7-cell (length == 0 or repeats == 0
// or length > N) — both cases are no-ops at dispatch time.
}
EffectOp.OP_1 -> { EffectOp.OP_1 -> {
// 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1). // 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1).
// bits 0-1 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick), // bits 0-1 (ff): 0=linear pitch, 1=Amiga period, 2=linear frequency (Hz/tick),
@@ -3054,11 +3125,18 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
playhead.position = ts.cuePos playhead.position = ts.cuePos
} }
// Per TAUD_NOTE_EFFECTS.md §S$Bx00: on pattern change reset loop_start_row and loop_count. // Per-pattern voice state reset, called on every cue advance (B / C / natural end).
// - S$Bx pattern-loop counters (TAUD_NOTE_EFFECTS.md §S$Bx00).
// - Pattern-ditto (effect 7) destination range — the source block lives in the
// pattern we are leaving and must not bleed into the next one (§7).
private fun resetPatternLoopState(ts: TrackerState) { private fun resetPatternLoopState(ts: TrackerState) {
for (voice in ts.voices) { for (voice in ts.voices) {
voice.loopStartRow = 0 voice.loopStartRow = 0
voice.loopCount = 0 voice.loopCount = 0
voice.dittoActive = false
voice.dittoSourceStart = 0
voice.dittoLength = 0
voice.dittoEndRow = 0
} }
} }
@@ -3599,6 +3677,17 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var loopStartRow = 0 var loopStartRow = 0
var loopCount = 0 var loopCount = 0
// Pattern ditto (effect 7) — per-channel state. See TAUD_NOTE_EFFECTS.md §7.
// dittoActive is the master gate; while true, rows in
// [dittoSourceStart + dittoLength .. dittoEndRow] are expanded by copying
// the cells from the source block (dittoSourceStart .. dittoSourceStart +
// dittoLength 1) and patching in any non-empty fields from the raw
// destination cell. All four reset on cue advance (B / C / natural end).
var dittoActive = false
var dittoSourceStart = 0
var dittoLength = 0
var dittoEndRow = 0
// Tempo slide (T $00xy) — per-channel because T is a per-channel effect, but we apply globally via playhead. // Tempo slide (T $00xy) — per-channel because T is a per-channel effect, but we apply globally via playhead.
var tempoSlideDir = 0 // 0 = none, -1 = down, +1 = up var tempoSlideDir = 0 // 0 = none, -1 = down, +1 = up
var tempoSlideAmount = 0 var tempoSlideAmount = 0
@@ -3841,6 +3930,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
it.glissandoOn = false it.glissandoOn = false
it.loopStartRow = 0 it.loopStartRow = 0
it.loopCount = 0 it.loopCount = 0
it.dittoActive = false
it.dittoSourceStart = 0
it.dittoLength = 0
it.dittoEndRow = 0
it.funkSpeed = 0 it.funkSpeed = 0
it.funkAccumulator = 0 it.funkAccumulator = 0
it.funkWritePos = 0 it.funkWritePos = 0