mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
Taud: sentinel values moved to negative octave range
This commit is contained in:
@@ -779,7 +779,7 @@ if V.dittoActive and armRow <= N <= V.dittoEndRow:
|
|||||||
srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength)
|
srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength)
|
||||||
src = patternRows[V.pattern][srcRow]
|
src = patternRows[V.pattern][srcRow]
|
||||||
|
|
||||||
cell.note = (raw.note != 0xFFFF) ? raw.note : src.note
|
cell.note = (raw.note != 0x0000) ? raw.note : src.note
|
||||||
cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument
|
cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument
|
||||||
|
|
||||||
# SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns;
|
# SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2020-2024 CuriousTorvald
|
Copyright (c) 2020-2026 CuriousTorvald
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
TVDOS (c) 2020-2024 CuriousTorvald
|
TVDOS (c) 2020-2026 CuriousTorvald
|
||||||
|
|
||||||
TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information.
|
TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information.
|
||||||
@@ -19,9 +19,9 @@ var Note = (function() {
|
|||||||
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
|
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.OFF = 0x0000; // key-off
|
t.NOP = 0x0000; // no-op (empty row)
|
||||||
t.CUT = 0xFFFE; // note cut (immediate)
|
t.OFF = 0x0001; // key-off
|
||||||
t.NOP = 0xFFFF; // no-op (empty row)
|
t.CUT = 0x0002; // note cut (immediate)
|
||||||
return t;
|
return t;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
||||||
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 === 0x0000 || note === 0x0001 || note === 0x0002 || (note >= 0x0010 && note <= 0x001F)) continue
|
||||||
// Use the full absolute pitch as tonic; the modular ops
|
// Use the full absolute pitch as tonic; the modular ops
|
||||||
// in _cadTension / _harmonicCost normalise it.
|
// in _cadTension / _harmonicCost normalise it.
|
||||||
tonic = note
|
tonic = note
|
||||||
@@ -476,7 +476,7 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
||||||
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 === 0x0000 || note === 0x0001 || note === 0x0002 || (note >= 0x0010 && note <= 0x001F)) continue
|
||||||
const origAbs = note
|
const origAbs = note
|
||||||
let newAbs
|
let newAbs
|
||||||
if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) {
|
if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) {
|
||||||
@@ -490,7 +490,7 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
for (let r = row + 1; r < ROWS_PER_PAT; r++) {
|
for (let r = row + 1; r < ROWS_PER_PAT; r++) {
|
||||||
const noff = 8 * r
|
const noff = 8 * r
|
||||||
const n = ptn[noff] | (ptn[noff+1] << 8)
|
const n = ptn[noff] | (ptn[noff+1] << 8)
|
||||||
if (n !== 0x0000) break
|
if (n !== 0x0001) break
|
||||||
duration++
|
duration++
|
||||||
}
|
}
|
||||||
lambda = 1 - Math.exp(-(duration - 1) / 4)
|
lambda = 1 - Math.exp(-(duration - 1) / 4)
|
||||||
@@ -558,9 +558,10 @@ Number.prototype.decD2 = function() {
|
|||||||
|
|
||||||
|
|
||||||
function noteToStr(note) {
|
function noteToStr(note) {
|
||||||
if (note === 0xFFFF) return sym.middot.repeat(4)
|
if (note === 0x0000) return sym.middot.repeat(4)
|
||||||
if (note === 0xFFFE) return sym.notecut
|
if (note === 0x0001) return sym.keyoff
|
||||||
if (note === 0x0000) return sym.keyoff
|
if (note === 0x0002) return sym.notecut
|
||||||
|
if (note >= 0x0010 && note <= 0x001F) return ('Int' + (note & 0xF).toString(16).toUpperCase()).padEnd(4)
|
||||||
const preset = pitchTablePresets[PITCH_PRESET_IDX]
|
const preset = pitchTablePresets[PITCH_PRESET_IDX]
|
||||||
if (preset.table.length === 0) return note.hex04()
|
if (preset.table.length === 0) return note.hex04()
|
||||||
const [period, offset] = decomposeNote(note, preset.interval)
|
const [period, offset] = decomposeNote(note, preset.interval)
|
||||||
@@ -656,7 +657,7 @@ const EMPTY_CELL = {
|
|||||||
sPanArg: sym.middot.repeat(2),
|
sPanArg: sym.middot.repeat(2),
|
||||||
sEffOp: sym.middot,
|
sEffOp: sym.middot,
|
||||||
sEffArg: sym.middot.repeat(4),
|
sEffArg: sym.middot.repeat(4),
|
||||||
_note: 0xFFFF, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0
|
_note: 0x0000, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCellAt(y, x, cell, back) {
|
function drawCellAt(y, x, cell, back) {
|
||||||
@@ -692,7 +693,7 @@ function drawCellAtStyled(y, x, cell, back, style) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)]
|
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)]
|
||||||
const noteEmpty = (cell._note === 0xFFFF)
|
const noteEmpty = (cell._note === 0x0000)
|
||||||
const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
|
const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
|
||||||
const volEmpty = (cell._voleff === 0)
|
const volEmpty = (cell._voleff === 0)
|
||||||
const panEmpty = (cell._paneff === 0)
|
const panEmpty = (cell._paneff === 0)
|
||||||
@@ -1537,8 +1538,8 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
|
|||||||
if (cumState !== null && lowerH > 0) {
|
if (cumState !== null && lowerH > 0) {
|
||||||
const _apo = Math.abs(cumState.pitchOff)
|
const _apo = Math.abs(cumState.pitchOff)
|
||||||
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
|
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
|
||||||
const _absN = (cumState.lastNote !== 0xFFFF && cumState.pitchOff !== 0)
|
const _absN = (cumState.lastNote !== 0x0000 && cumState.pitchOff !== 0)
|
||||||
? noteToStr(Math.max(0, Math.min(0xFFFE, cumState.lastNote + cumState.pitchOff))) + ' '
|
? noteToStr(Math.max(0x20, Math.min(0xFFFF, cumState.lastNote + cumState.pitchOff))) + ' '
|
||||||
: ''
|
: ''
|
||||||
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
|
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
|
||||||
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
|
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
|
||||||
@@ -2242,7 +2243,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
0x0000, 0x0023, 0x0046, 0x0074, 0x0098, 0x00C8, 0x00F9, 0x0110
|
0x0000, 0x0023, 0x0046, 0x0074, 0x0098, 0x00C8, 0x00F9, 0x0110
|
||||||
]
|
]
|
||||||
|
|
||||||
let lastNote = 0xFFFF, lastInst = 0
|
let lastNote = 0x0000, lastInst = 0
|
||||||
let volAbs = 0x3F // 6-bit per-note volume (engine: noteVolume axis;
|
let volAbs = 0x3F // 6-bit per-note volume (engine: noteVolume axis;
|
||||||
// M / N's per-channel axis is not modelled here)
|
// M / N's per-channel axis is not modelled here)
|
||||||
let panAbs = 0x80 // 8-bit channel pan (engine width); centre = $80
|
let panAbs = 0x80 // 8-bit channel pan (engine width); centre = $80
|
||||||
@@ -2295,8 +2296,8 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
// not tracked by this simulator. The simulator approximates the seed
|
// not tracked by this simulator. The simulator approximates the seed
|
||||||
// as 0x3F (legacy fallback) — see the longer note below.
|
// as 0x3F (legacy fallback) — see the longer note below.
|
||||||
let reloadDefaultVol = false
|
let reloadDefaultVol = false
|
||||||
if (note !== 0xFFFF && note !== 0xFFFE) {
|
if (note !== 0x0000 && note !== 0x0002 && !(note >= 0x0010 && note <= 0x001F)) {
|
||||||
if (note === 0x0000) {
|
if (note === 0x0001) {
|
||||||
// key-off; sample stays referenced
|
// key-off; sample stays referenced
|
||||||
} else if (isGRow) {
|
} else if (isGRow) {
|
||||||
portaTarget = note
|
portaTarget = note
|
||||||
@@ -2419,7 +2420,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
}
|
}
|
||||||
else if (effop === OP_G) {
|
else if (effop === OP_G) {
|
||||||
if (effarg !== 0) memG = effarg
|
if (effarg !== 0) memG = effarg
|
||||||
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0xFFFF) {
|
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0x0000) {
|
||||||
const curPitch = lastNote + pitchOff
|
const curPitch = lastNote + pitchOff
|
||||||
const diff = portaTarget - curPitch
|
const diff = portaTarget - curPitch
|
||||||
if (diff !== 0) {
|
if (diff !== 0) {
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ def encode_note_it(it_note: int) -> int:
|
|||||||
# IT C-5 anchors to Taud C-4, so offset = it_note - 60.
|
# IT C-5 anchors to Taud C-4, so offset = it_note - 60.
|
||||||
semis = it_note - 60
|
semis = it_note - 60
|
||||||
val = round(TAUD_C4 + semis * 4096 / 12)
|
val = round(TAUD_C4 + semis * 4096 / 12)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ def period_to_taud_note(period: int) -> int:
|
|||||||
if period <= 0:
|
if period <= 0:
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
|
val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
|
|
||||||
|
|
||||||
# ── PT effect → Taud effect ──────────────────────────────────────────────────
|
# ── PT effect → Taud effect ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ def mon_note_to_taud(mon_note: int) -> int:
|
|||||||
if mon_note == 0x7F:
|
if mon_note == 0x7F:
|
||||||
return NOTE_CUT
|
return NOTE_CUT
|
||||||
val = TAUD_C4 + round((mon_note - MON_NOTE_C4) * 4096.0 / 12.0)
|
val = TAUD_C4 + round((mon_note - MON_NOTE_C4) * 4096.0 / 12.0)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
|
|
||||||
|
|
||||||
# ── Effect mapping (Monotone 3-bit code + 6-bit data → Taud) ─────────────────
|
# ── Effect mapping (Monotone 3-bit code + 6-bit data → Taud) ─────────────────
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ def encode_note(s3m_note: int) -> int:
|
|||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
semitones = (octave - 4) * 12 + pitch
|
semitones = (octave - 4) * 12 + pitch
|
||||||
val = round(TAUD_C4 + semitones * 4096 / 12)
|
val = round(TAUD_C4 + semitones * 4096 / 12)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
|
|
||||||
|
|
||||||
def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ NUM_VOICES = 20
|
|||||||
SAMPLE_LEN_LIMIT = 65535
|
SAMPLE_LEN_LIMIT = 65535
|
||||||
|
|
||||||
# Note word sentinels
|
# Note word sentinels
|
||||||
NOTE_NOP = 0xFFFF
|
NOTE_NOP = 0x0000
|
||||||
NOTE_KEYOFF = 0x0000
|
NOTE_KEYOFF = 0x0001
|
||||||
NOTE_CUT = 0xFFFE
|
NOTE_CUT = 0x0002
|
||||||
TAUD_C4 = 0x5000 # The audio engine's Middle C
|
TAUD_C4 = 0x5000 # The audio engine's Middle C
|
||||||
|
|
||||||
# Cue sheet instruction byte (cue offset 30; offset 31 = arg byte for 2-byte forms).
|
# Cue sheet instruction byte (cue offset 30; offset 31 = arg byte for 2-byte forms).
|
||||||
|
|||||||
@@ -2255,7 +2255,7 @@ from source.
|
|||||||
* Semantics (matches IT/Schism player/effects.c:1664-1764 csf_check_nna):
|
* Semantics (matches IT/Schism player/effects.c:1664-1764 csf_check_nna):
|
||||||
- Fires on every fresh foreground note trigger on a channel, BEFORE the
|
- Fires on every fresh foreground note trigger on a channel, BEFORE the
|
||||||
NNA-spawn step that would ghost the existing voice. Does NOT fire on
|
NNA-spawn step that would ghost the existing voice. Does NOT fire on
|
||||||
tone portamento, on note-off (0x0000), on note-cut (0xFFFE), or on
|
tone portamento, on note-off (0x0001), on note-cut (0x0002), or on
|
||||||
empty cells.
|
empty cells.
|
||||||
- The DCT/DCA values consulted belong to the EXISTING voice's instrument
|
- The DCT/DCA values consulted belong to the EXISTING voice's instrument
|
||||||
(i.e. the OLD note's instrument, not the incoming note's). Different
|
(i.e. the OLD note's instrument, not the incoming note's). Different
|
||||||
@@ -2401,6 +2401,8 @@ TODO:
|
|||||||
[x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix
|
[x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix
|
||||||
[x] do not reset tickspeed on pattern view play / add key to modify tick speed ('[' down/']' up)
|
[x] do not reset tickspeed on pattern view play / add key to modify tick speed ('[' down/']' up)
|
||||||
[x] expose song table on UI (test with `insaniq2.taud`)
|
[x] expose song table on UI (test with `insaniq2.taud`)
|
||||||
|
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
||||||
|
[ ] establish hooks for the interrupts
|
||||||
|
|
||||||
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
|
||||||
@@ -2421,13 +2423,14 @@ Play Data: play data are series of tracker-like instructions, visualised as:
|
|||||||
rr||NOTE|Ins|E.Vol|E.Pan|EE.ff|
|
rr||NOTE|Ins|E.Vol|E.Pan|EE.ff|
|
||||||
63||FFFF|255|3 63|3 63|FF FFFF| (8 bytes per line, 512 bytes per pattern, 128 patterns on 64 kB bank, 32 banks available (pattern 0xFFF -- bank 31, pattern 127 is a sentinel value for no-pattern))
|
63||FFFF|255|3 63|3 63|FF FFFF| (8 bytes per line, 512 bytes per pattern, 128 patterns on 64 kB bank, 32 banks available (pattern 0xFFF -- bank 31, pattern 127 is a sentinel value for no-pattern))
|
||||||
|
|
||||||
notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value.
|
notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value. 0x1000: C at zeroth octave; 0xF000: C at 14th octave; 0xFFFF: ~C at 15th octave; 0x0000..0x001F: reserved for sentinels (valid playable note range is 0x0020..0xFFFF)
|
||||||
|
|
||||||
Special values:
|
Special values:
|
||||||
|
|
||||||
note 0xFFFF: no-op
|
note 0x0000: no-op
|
||||||
note 0xFFFE: note cut
|
note 0x0001: key-off
|
||||||
note 0x0000: key-off
|
note 0x0002: note cut
|
||||||
|
note 0x0010..0x001F: Interrupt 0..F (notation: Int0..IntF) — reserved interrupt slots; engine has no default handler.
|
||||||
|
|
||||||
inst 0: no instrument change
|
inst 0: no instrument change
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import net.torvald.tsvm.peripheral.MP2Env
|
|||||||
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
|
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
|
||||||
*
|
*
|
||||||
* Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave.
|
* Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave.
|
||||||
* Empty row: note = 0xFFFF (no trigger). All 256 instrument slots (0-255) are valid.
|
* Empty row: note = 0x0000 (no trigger). Note sentinels (0x0000..0x001F): 0x0000 = no-op,
|
||||||
|
* 0x0001 = key-off, 0x0002 = note cut, 0x0010..0x001F = Int0..IntF (reserved interrupts).
|
||||||
|
* Valid playable notes are 0x0020..0xFFFF. All 256 instrument slots (0-255) are valid.
|
||||||
*
|
*
|
||||||
* ## How to upload PCM audio into a playhead
|
* ## How to upload PCM audio into a playhead
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
|
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
|
||||||
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
|
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
|
||||||
internal val instruments = Array(256) { TaudInst(it) }
|
internal val instruments = Array(256) { TaudInst(it) }
|
||||||
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } }
|
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0x0000, 0, 0, 0, 32, 0, 0, 0) } }
|
||||||
internal val playheads: Array<Playhead>
|
internal val playheads: Array<Playhead>
|
||||||
internal val cueSheet = Array(1024) { PlayCue() }
|
internal val cueSheet = Array(1024) { PlayCue() }
|
||||||
internal val pcmBin = arrayOf(
|
internal val pcmBin = arrayOf(
|
||||||
@@ -2275,7 +2275,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TaudPlayData(
|
TaudPlayData(
|
||||||
note = if (rawRow.note != 0xFFFF) rawRow.note else src.note,
|
note = if (rawRow.note != 0x0000) rawRow.note else src.note,
|
||||||
instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment,
|
instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment,
|
||||||
volume = if (volIsSet) rawRow.volume else src.volume,
|
volume = if (volIsSet) rawRow.volume else src.volume,
|
||||||
volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff,
|
volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff,
|
||||||
@@ -2329,7 +2329,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`)
|
// physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`)
|
||||||
// silences after the first row because the slide saturates at 0 and there's
|
// silences after the first row because the slide saturates at 0 and there's
|
||||||
// nothing to lift the volume back up before the next slide starts.
|
// nothing to lift the volume back up before the next slide starts.
|
||||||
0xFFFF -> {
|
0x0000 -> {
|
||||||
if (row.instrment != 0) {
|
if (row.instrment != 0) {
|
||||||
voice.instrumentId = row.instrment
|
voice.instrumentId = row.instrment
|
||||||
val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId])
|
val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId])
|
||||||
@@ -2345,8 +2345,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting
|
// fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting
|
||||||
// voice.active = false here would defeat both — instruments with sustain points
|
// voice.active = false here would defeat both — instruments with sustain points
|
||||||
// and non-zero fadeout (FT2 sustain-then-fade idiom) would be cut on the spot.
|
// and non-zero fadeout (FT2 sustain-then-fade idiom) would be cut on the spot.
|
||||||
0x0000 -> { voice.keyOff = true }
|
0x0001 -> { voice.keyOff = true }
|
||||||
0xFFFE -> voice.active = false // note cut (immediate)
|
0x0002 -> voice.active = false // note cut (immediate)
|
||||||
|
in 0x0003..0x000F -> { /* reserved sentinel range, no engine handler */ }
|
||||||
|
in 0x0010..0x001F -> { /* Int0..IntF: reserved interrupt slots, no engine handler yet */ }
|
||||||
else -> {
|
else -> {
|
||||||
if (toneG && voice.active) {
|
if (toneG && voice.active) {
|
||||||
// Tone porta: target the note, do not retrigger sample.
|
// Tone porta: target the note, do not retrigger sample.
|
||||||
@@ -2502,7 +2504,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period
|
1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period
|
||||||
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
|
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
|
||||||
else -> voice.noteVal - mag // linear 4096-TET
|
else -> voice.noteVal - mag // linear 4096-TET
|
||||||
}.coerceIn(1, 0xFFFD)
|
}.coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
|
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
|
||||||
voice.linearFreq = -1.0
|
voice.linearFreq = -1.0
|
||||||
@@ -2521,7 +2523,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
1 -> amigaSlideOnce(voice.noteVal, mag)
|
1 -> amigaSlideOnce(voice.noteVal, mag)
|
||||||
2 -> linearFreqSlideOnce(voice.noteVal, mag)
|
2 -> linearFreqSlideOnce(voice.noteVal, mag)
|
||||||
else -> voice.noteVal + mag
|
else -> voice.noteVal + mag
|
||||||
}.coerceIn(1, 0xFFFD)
|
}.coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0
|
voice.amigaPeriod = -1.0
|
||||||
voice.linearFreq = -1.0
|
voice.linearFreq = -1.0
|
||||||
@@ -2730,7 +2732,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
0x1 -> voice.glissandoOn = (x != 0)
|
0x1 -> voice.glissandoOn = (x != 0)
|
||||||
0x2 -> {
|
0x2 -> {
|
||||||
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(1, 0xFFFD)
|
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0
|
voice.amigaPeriod = -1.0
|
||||||
voice.linearFreq = -1.0
|
voice.linearFreq = -1.0
|
||||||
@@ -2832,7 +2834,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
1 -> amigaSlideTick(voice, voice.slideArg)
|
1 -> amigaSlideTick(voice, voice.slideArg)
|
||||||
2 -> linearFreqSlideTick(voice, voice.slideArg)
|
2 -> linearFreqSlideTick(voice, voice.slideArg)
|
||||||
else -> voice.noteVal + voice.slideArg
|
else -> voice.noteVal + voice.slideArg
|
||||||
}.coerceIn(1, 0xFFFD)
|
}.coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2854,7 +2856,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
voice.noteVal = target
|
voice.noteVal = target
|
||||||
voice.tonePortaTarget = -1
|
voice.tonePortaTarget = -1
|
||||||
} else {
|
} else {
|
||||||
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(1, 0xFFFD)
|
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(0x20, 0xFFFF)
|
||||||
}
|
}
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0
|
voice.amigaPeriod = -1.0
|
||||||
@@ -2912,14 +2914,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
if (voice.vibratoActive) {
|
if (voice.vibratoActive) {
|
||||||
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
|
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
|
||||||
val pitchDelta = (sine * voice.mem.huDepth) shr voice.vibratoFineShift
|
val pitchDelta = (sine * voice.mem.huDepth) shr voice.vibratoFineShift
|
||||||
pitchToMixer = (voice.noteVal + pitchDelta).coerceIn(1, 0xFFFD)
|
pitchToMixer = (voice.noteVal + pitchDelta).coerceIn(0x20, 0xFFFF)
|
||||||
voice.vibratoLfoPos = (voice.vibratoLfoPos + voice.mem.huSpeed * 4) and 0xFF
|
voice.vibratoLfoPos = (voice.vibratoLfoPos + voice.mem.huSpeed * 4) and 0xFF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
|
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
|
||||||
if (voice.glissandoOn) {
|
if (voice.glissandoOn) {
|
||||||
val semis = ((pitchToMixer * 12 + 2048) / 4096)
|
val semis = ((pitchToMixer * 12 + 2048) / 4096)
|
||||||
pitchToMixer = (semis * 4096 / 12).coerceIn(1, 0xFFFD)
|
pitchToMixer = (semis * 4096 / 12).coerceIn(0x20, 0xFFFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tremolo (R) — modulates rowVolume around the per-note volume base. IT's tremolo
|
// Tremolo (R) — modulates rowVolume around the per-note volume base. IT's tremolo
|
||||||
@@ -2946,7 +2948,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
if (voice.arpActive) {
|
if (voice.arpActive) {
|
||||||
val voiceIdx = ts.tickInRow % 3
|
val voiceIdx = ts.tickInRow % 3
|
||||||
val arpDelta = when (voiceIdx) { 1 -> voice.arpOff1 shl 8; 2 -> voice.arpOff2 shl 8; else -> 0 }
|
val arpDelta = when (voiceIdx) { 1 -> voice.arpOff1 shl 8; 2 -> voice.arpOff2 shl 8; else -> 0 }
|
||||||
pitchToMixer = (voice.basePitch + arpDelta).coerceIn(1, 0xFFFD)
|
pitchToMixer = (voice.basePitch + arpDelta).coerceIn(0x20, 0xFFFF)
|
||||||
voice.lastArpVoice = voiceIdx
|
voice.lastArpVoice = voiceIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2983,7 +2985,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
||||||
else 0
|
else 0
|
||||||
|
|
||||||
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD)
|
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
|
||||||
voice.playbackRate = computePlaybackRate(inst, finalPitch)
|
voice.playbackRate = computePlaybackRate(inst, finalPitch)
|
||||||
|
|
||||||
// Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
|
// Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
|
||||||
@@ -3087,7 +3089,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
|
val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
|
||||||
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
||||||
else 0
|
else 0
|
||||||
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD)
|
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
|
||||||
bg.playbackRate = computePlaybackRate(inst, finalPitch)
|
bg.playbackRate = computePlaybackRate(inst, finalPitch)
|
||||||
// Filter-mode pf envelope: same scaling rule as foreground.
|
// Filter-mode pf envelope: same scaling rule as foreground.
|
||||||
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
|
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
|
||||||
@@ -3603,7 +3605,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var randomPanBias = 0 // signed
|
var randomPanBias = 0 // signed
|
||||||
|
|
||||||
// Pitch state (4096-TET units, signed when slid).
|
// Pitch state (4096-TET units, signed when slid).
|
||||||
var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added)
|
var noteVal = 0x0000 // The currently sounding base note (no per-row vibrato/arp added); 0 = none yet
|
||||||
var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay
|
var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay
|
||||||
// Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose
|
// Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose
|
||||||
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
|
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
|
||||||
@@ -3965,7 +3967,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
it.hasPfEnv = false; it.envPfIsFilter = false
|
it.hasPfEnv = false; it.envPfIsFilter = false
|
||||||
it.fadeoutVolume = 1.0
|
it.fadeoutVolume = 1.0
|
||||||
it.rampOutSamples = 0; it.rampOutGain = 0.0; it.rampOutStep = 0.0
|
it.rampOutSamples = 0; it.rampOutGain = 0.0; it.rampOutStep = 0.0
|
||||||
it.noteVal = 0xFFFF; it.basePitch = 0x4000
|
it.noteVal = 0x0000; it.basePitch = 0x4000
|
||||||
it.amigaPeriod = -1.0; it.linearFreq = -1.0
|
it.amigaPeriod = -1.0; it.linearFreq = -1.0
|
||||||
it.tonePortaTarget = -1; it.tonePortaSpeed = 0
|
it.tonePortaTarget = -1; it.tonePortaSpeed = 0
|
||||||
it.filterY1 = 0.0; it.filterY2 = 0.0
|
it.filterY1 = 0.0; it.filterY2 = 0.0
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ def encode_note_xm(xm_note: int) -> int:
|
|||||||
if 1 <= xm_note <= 96:
|
if 1 <= xm_note <= 96:
|
||||||
semis = xm_note - XM_RELNOTE_C4
|
semis = xm_note - XM_RELNOTE_C4
|
||||||
val = round(TAUD_C4 + semis * 4096 / 12)
|
val = round(TAUD_C4 + semis * 4096 / 12)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user