Taud: sentinel values moved to negative octave range

This commit is contained in:
minjaesong
2026-05-16 19:33:17 +09:00
parent 00c0e18c1a
commit e6f77c4789
14 changed files with 59 additions and 51 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
}()); }());

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 ──────────────────────────────────────────────────

View File

@@ -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) ─────────────────

View File

@@ -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,

View File

@@ -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).

View File

@@ -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

View File

@@ -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
* *

View File

@@ -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

View File

@@ -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