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

View File

@@ -19,9 +19,9 @@ var Note = (function() {
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
}
}
t.OFF = 0x0000; // key-off
t.CUT = 0xFFFE; // note cut (immediate)
t.NOP = 0xFFFF; // no-op (empty row)
t.NOP = 0x0000; // no-op (empty row)
t.OFF = 0x0001; // key-off
t.CUT = 0x0002; // note cut (immediate)
return t;
}());

View File

@@ -466,7 +466,7 @@ function retuneAllPatterns(newIdx, method) {
for (let row = 0; row < ROWS_PER_PAT; row++) {
const off = 8 * row
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
// in _cadTension / _harmonicCost normalise it.
tonic = note
@@ -476,7 +476,7 @@ function retuneAllPatterns(newIdx, method) {
for (let row = 0; row < ROWS_PER_PAT; row++) {
const off = 8 * row
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
let newAbs
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++) {
const noff = 8 * r
const n = ptn[noff] | (ptn[noff+1] << 8)
if (n !== 0x0000) break
if (n !== 0x0001) break
duration++
}
lambda = 1 - Math.exp(-(duration - 1) / 4)
@@ -558,9 +558,10 @@ Number.prototype.decD2 = function() {
function noteToStr(note) {
if (note === 0xFFFF) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut
if (note === 0x0000) return sym.keyoff
if (note === 0x0000) return sym.middot.repeat(4)
if (note === 0x0001) 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]
if (preset.table.length === 0) return note.hex04()
const [period, offset] = decomposeNote(note, preset.interval)
@@ -656,7 +657,7 @@ const EMPTY_CELL = {
sPanArg: sym.middot.repeat(2),
sEffOp: sym.middot,
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) {
@@ -692,7 +693,7 @@ function drawCellAtStyled(y, x, cell, back, style) {
return
}
// 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 volEmpty = (cell._voleff === 0)
const panEmpty = (cell._paneff === 0)
@@ -1537,8 +1538,8 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
if (cumState !== null && lowerH > 0) {
const _apo = Math.abs(cumState.pitchOff)
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
const _absN = (cumState.lastNote !== 0xFFFF && cumState.pitchOff !== 0)
? noteToStr(Math.max(0, Math.min(0xFFFE, cumState.lastNote + cumState.pitchOff))) + ' '
const _absN = (cumState.lastNote !== 0x0000 && cumState.pitchOff !== 0)
? noteToStr(Math.max(0x20, Math.min(0xFFFF, cumState.lastNote + cumState.pitchOff))) + ' '
: ''
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
@@ -2242,7 +2243,7 @@ function simulateRowState(ptnDat, uptoRow) {
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;
// M / N's per-channel axis is not modelled here)
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
// as 0x3F (legacy fallback) — see the longer note below.
let reloadDefaultVol = false
if (note !== 0xFFFF && note !== 0xFFFE) {
if (note === 0x0000) {
if (note !== 0x0000 && note !== 0x0002 && !(note >= 0x0010 && note <= 0x001F)) {
if (note === 0x0001) {
// key-off; sample stays referenced
} else if (isGRow) {
portaTarget = note
@@ -2419,7 +2420,7 @@ function simulateRowState(ptnDat, uptoRow) {
}
else if (effop === OP_G) {
if (effarg !== 0) memG = effarg
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0xFFFF) {
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0x0000) {
const curPitch = lastNote + pitchOff
const diff = portaTarget - curPitch
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.
semis = it_note - 60
val = round(TAUD_C4 + semis * 4096 / 12)
return max(1, min(0xFFFD, val))
return max(0x20, min(0xFFFF, val))
return NOTE_NOP

View File

@@ -250,7 +250,7 @@ def period_to_taud_note(period: int) -> int:
if period <= 0:
return NOTE_NOP
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 ──────────────────────────────────────────────────

View File

@@ -139,7 +139,7 @@ def mon_note_to_taud(mon_note: int) -> int:
if mon_note == 0x7F:
return NOTE_CUT
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) ─────────────────

View File

@@ -234,7 +234,7 @@ def encode_note(s3m_note: int) -> int:
return NOTE_NOP
semitones = (octave - 4) * 12 + pitch
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,

View File

@@ -96,9 +96,9 @@ NUM_VOICES = 20
SAMPLE_LEN_LIMIT = 65535
# Note word sentinels
NOTE_NOP = 0xFFFF
NOTE_KEYOFF = 0x0000
NOTE_CUT = 0xFFFE
NOTE_NOP = 0x0000
NOTE_KEYOFF = 0x0001
NOTE_CUT = 0x0002
TAUD_C4 = 0x5000 # The audio engine's Middle C
# 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):
- 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
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.
- 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
@@ -2401,6 +2401,8 @@ TODO:
[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] 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:
* 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|
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:
note 0xFFFF: no-op
note 0xFFFE: note cut
note 0x0000: key-off
note 0x0000: no-op
note 0x0001: 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

View File

@@ -23,7 +23,9 @@ import net.torvald.tsvm.peripheral.MP2Env
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
*
* 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
*

View File

@@ -243,7 +243,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
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 cueSheet = Array(1024) { PlayCue() }
internal val pcmBin = arrayOf(
@@ -2275,7 +2275,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
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,
volume = if (volIsSet) rawRow.volume else src.volume,
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`)
// 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.
0xFFFF -> {
0x0000 -> {
if (row.instrment != 0) {
voice.instrumentId = row.instrment
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
// 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.
0x0000 -> { voice.keyOff = true }
0xFFFE -> voice.active = false // note cut (immediate)
0x0001 -> { voice.keyOff = true }
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 -> {
if (toneG && voice.active) {
// 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
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
else -> voice.noteVal - mag // linear 4096-TET
}.coerceIn(1, 0xFFFD)
}.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
voice.linearFreq = -1.0
@@ -2521,7 +2523,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideOnce(voice.noteVal, mag)
2 -> linearFreqSlideOnce(voice.noteVal, mag)
else -> voice.noteVal + mag
}.coerceIn(1, 0xFFFD)
}.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0
voice.linearFreq = -1.0
@@ -2730,7 +2732,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
0x1 -> voice.glissandoOn = (x != 0)
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.amigaPeriod = -1.0
voice.linearFreq = -1.0
@@ -2832,7 +2834,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideTick(voice, voice.slideArg)
2 -> linearFreqSlideTick(voice, voice.slideArg)
else -> voice.noteVal + voice.slideArg
}.coerceIn(1, 0xFFFD)
}.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal
}
@@ -2854,7 +2856,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.noteVal = target
voice.tonePortaTarget = -1
} else {
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(1, 0xFFFD)
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(0x20, 0xFFFF)
}
voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0
@@ -2912,14 +2914,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.vibratoActive) {
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
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
}
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
if (voice.glissandoOn) {
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
@@ -2946,7 +2948,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.arpActive) {
val voiceIdx = ts.tickInRow % 3
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
}
@@ -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()
else 0
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD)
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
voice.playbackRate = computePlaybackRate(inst, finalPitch)
// 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)
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
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)
// Filter-mode pf envelope: same scaling rule as foreground.
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
@@ -3603,7 +3605,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var randomPanBias = 0 // signed
// 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
// 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).
@@ -3965,7 +3967,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
it.hasPfEnv = false; it.envPfIsFilter = false
it.fadeoutVolume = 1.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.tonePortaTarget = -1; it.tonePortaSpeed = 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:
semis = xm_note - XM_RELNOTE_C4
val = round(TAUD_C4 + semis * 4096 / 12)
return max(1, min(0xFFFD, val))
return max(0x20, min(0xFFFF, val))
return NOTE_NOP