taut: two new effects

This commit is contained in:
minjaesong
2026-06-17 01:01:08 +09:00
parent f617b17506
commit 35fb833835
3 changed files with 180 additions and 40 deletions

View File

@@ -793,6 +793,26 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr
---
## 5 $xxyy and 6 $xxyy — Filter Cutoff/Resonance Control
**Plain.** `5` sets the cutoff and `6` sets the resonance of the instrument's filter directly. When the filter is in ImpulseTracker mode, only the high byte (the `xx` part) is read; when the filter is in SoundFont2 mode, both bytes are read. Argument `$FFFF` resets the parameter to its default value (for both IT and SF2 mode). Every note that shares the instrument is affected — the change is **instrument-wide**, not per-voice. If cutoff vibrato is what you are after, modify the filter envelope directly.
**Compatibility.** Unique to Taud — no ST3/IT/PT equivalent. The effect has **no memory** (`$0000` is a literal "set to zero", not a recall).
**Implementation.** The effect writes a per-instrument **cutoff / resonance override** that supersedes the value loaded from the instrument record (bytes 182/183, plus 252/253 in SF mode). The argument is decoded in the instrument's active filter mode:
- **ImpulseTracker mode:** only the high byte `$xx` is read (0..254 active; 255 = filter off), matching the 8-bit cutoff/resonance storage.
- **SoundFont2 mode:** the full 16-bit argument `$xxyy` is read (cutoff in absolute cents, resonance in centibels), matching the 16-bit storage.
- **`$FFFF`** clears the override, restoring the value loaded from the record. The engine **MUST** test for `$FFFF` *before* the mode split, so it is always the reset sentinel regardless of filter mode.
Because the override is instrument-wide, an engine **MUST** apply it to **every note that is already sounding** on that instrument — not only to notes triggered afterwards. The reference engine does this in two parts: (a) it stores the override on the instrument so subsequent triggers seed from it, and (b) it walks the live foreground voices and background ghosts and re-seeds the cutoff/resonance of every voice bound to the affected instrument, forcing a filter-coefficient refresh. A voice with a filter **envelope** recomputes its working cutoff from the (now-overridden) default each tick, so the envelope sweep is rescaled to the new base; a voice without one reads the overridden value directly.
This effect applies to ordinary instruments. When used on a **metainstrument**, the override **MUST** be applied to the constituent instruments all at once — the reference engine fans the write out across the foreground layer plus every layer-child voice sounding on the channel, so the whole stack moves together.
The override is **runtime state**: it persists across rows and pattern boundaries within one playback, but **MUST** be cleared when the song is restarted (so a loop or replay begins from the file defaults) and when a fresh instrument record is uploaded into the slot.
---
## 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.
@@ -1114,8 +1134,12 @@ S $6x and S $Ex are orthogonal: when S $Ex is active the current row repeats `$x
| $A | Panning Envelope On | Enables the currently active note's panning envelope |
| $B | Pitch Envelope Off | Disables the currently active note's pitch or filter envelope |
| $C | Pitch Envelope On | Enables the currently active note's pitch envelope |
| $D | Filter Envelope Off | Disables the currently active note's filter envelope |
| $E | Filter Envelope On | Enables the currently active note's filter envelope |
**Compatibility.** IT `S7x` maps directly.
When the instrument have both pitch and filter envelopes defined, $B/$C toggles pitch envelope only.
**Compatibility.** For $x in 0..$C, IT `S7x` maps directly. $D and $E differs from MPTM and unique to Taud
**Implementation.** Engines maintain a *mixer-private* background-voice pool per playhead, separate from the addressable foreground voices. When a fresh note retriggers a still-active foreground voice, the engine reads the effective NNA — the per-voice override set by `S $73..$76` if present, otherwise the instrument's default NNA (instrument record byte 186, low two bits) — and acts on the displaced voice as follows:
@@ -1132,7 +1156,18 @@ The background pool is reaped when a ghost's `fadeoutVolume` drops to zero or it
`S $73..$76` write the per-voice NNA override on the **currently active foreground voice** so that *its* next NNA event uses the overridden action. The override is cleared on every fresh trigger.
`S $77..$7C` toggle the volume / panning / pitch-or-filter envelope on the currently active voice. While disabled, the envelope is frozen (no advancement) and the mixer treats its contribution as unity (envVolume / envPan / envPfValue all replaced by the neutral 1.0 / 0.5 / 0.5).
`S $77..$7E` toggle an envelope on the currently active voice. The engine **MUST** keep **four independent gates** — volume, panning, pitch, filter — so the four pairs act on disjoint state:
- `$77 / $78` — volume envelope off / on.
- `$79 / $7A` — panning envelope off / on.
- `$7B / $7C`**pitch** envelope off / on, *when the instrument defines a pitch envelope*. On an instrument that defines only a filter envelope (the IT case where the single pitch/filter slot is flagged as a filter env), `$7B / $7C` fall back to toggling that filter envelope — this is the IT "pitch or filter envelope" semantics. When the instrument defines **both** envelopes, `$7B / $7C` toggle the pitch gate only and leave the filter gate untouched.
- `$7D / $7E`**filter** envelope off / on (Taud-specific; differs from MPTM). These always target the filter gate regardless of what else is defined.
While a gate is disabled the corresponding envelope is frozen (no advancement) and the mixer treats its contribution as unity (volume / pan / pitch / filter value replaced by the neutral 1.0 / 0.5 / 0.5 / 0.5).
Because the engine resolves the byte-19 and byte-197 envelope slots into explicit pitch and filter roles at trigger time (by reading each slot's `m`-bit — the slot order is undefined: on some songs offset 19 is the pitch env, on others it is the filter env), the `$7B`/`$7C` vs `$7D`/`$7E` dispatch reads those resolved roles directly and does not re-inspect the `m`-bits per event.
Effect $7..$E applies to ordinary instruments. When used on a metainstrument, the effect **MUST** be applied onto the constituent instruments all at once — the reference engine fans the toggle out across the foreground layer plus every layer-child voice sounding on the channel. Effect $0..$6 is a **no-op** on metainstruments: a live meta's layer-child voices are themselves background ghosts, so a Past-Note action ($70..$72) would otherwise cull the very layers that make up the sounding note.
---

View File

@@ -130,8 +130,8 @@ const fxNames = {
'2':"UNIMPLEMENTED",
'3':"UNIMPLEMENTED",
'4':"UNIMPLEMENTED",
'5':"UNIMPLEMENTED",
'6':"UNIMPLEMENTED",
'5':"Filter cutoff",
'6':"Filter reson.",
'7':"Pattern Ditto",
'8':"Bitcrusher ",
'9':"Overdrive ",

View File

@@ -1332,6 +1332,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
private object EffectOp {
const val OP_NONE = 0x00
const val OP_1 = 0x01
const val OP_5 = 0x05
const val OP_6 = 0x06
const val OP_7 = 0x07
const val OP_8 = 0x08
const val OP_9 = 0x09
@@ -1827,7 +1829,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
/** Advance the pitch envelope (drives playback rate; 0.5 = unity). */
private fun advancePitchEnvelope(voice: Voice, tickSec: Double) {
if (!voice.hasPitchEnv || !voice.pfEnvOn) return
if (!voice.hasPitchEnv || !voice.pitchEnvOn) return
pfIdxBox[0] = voice.envPitchIndex; pfTimeBox[0] = voice.envPitchTimeSec
voice.envPitchValue = advancePfRole(voice.activePitchEnv, voice.activePitchEnvLoop,
voice.activePitchEnvSustain, voice.keyOff, tickSec, pfWrap, pfIdxBox, pfTimeBox)
@@ -1836,7 +1838,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
/** Advance the filter envelope (drives cutoff; 0.5 = unity). */
private fun advanceFilterEnvelope(voice: Voice, tickSec: Double) {
if (!voice.hasFilterEnv || !voice.pfEnvOn) return
if (!voice.hasFilterEnv || !voice.filterEnvOn) return
pfIdxBox[0] = voice.envFilterIndex; pfTimeBox[0] = voice.envFilterTimeSec
voice.envFilterValue = advancePfRole(voice.activeFilterEnv, voice.activeFilterEnvLoop,
voice.activeFilterEnvSustain, voice.keyOff, tickSec, pfWrap, pfIdxBox, pfTimeBox)
@@ -2361,6 +2363,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.layerMixGain = META_MIX_GAIN[l0.mixOctet and 0xFF]
voice.layerRelDetune = 0
voice.isLayerChild = false
voice.metaForeground = true // marks the channel as playing a meta (S$7x fan-out / no-op)
for (k in 1 until layers.size) {
val lk = layers[k]
val child = Voice()
@@ -2507,11 +2510,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.volRampStep = 0.0
voice.noteWasCut = false
voice.noteFading = false
// S $73..$7C state resets on each fresh trigger so per-note overrides don't leak.
// S $73..$7E state resets on each fresh trigger so per-note overrides don't leak.
voice.nnaOverride = -1
voice.volEnvOn = true
voice.panEnvOn = true
voice.pfEnvOn = true
voice.pitchEnvOn = true
voice.filterEnvOn = true
// Default to "not a meta foreground"; triggerMetaOrNote re-sets this for the meta path.
voice.metaForeground = false
// Vibrato/tremolo/panbrello retrigger: reset LFO position when waveform requests it.
if (voice.vibratoRetrig) voice.vibratoLfoPos = 0
if (voice.tremoloRetrig) voice.tremoloLfoPos = 0
@@ -2674,7 +2680,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
v.linearFreq = src.linearFreq
v.volEnvOn = src.volEnvOn
v.panEnvOn = src.panEnvOn
v.pfEnvOn = src.pfEnvOn
v.pitchEnvOn = src.pitchEnvOn
v.filterEnvOn = src.filterEnvOn
v.metaForeground = src.metaForeground
v.noteFading = src.noteFading
// Keep the source's Metainstrument layer-0 mix gain on the ghost so an NNA tail of
// a layered note fades at the same level it was sounding (isLayerChild stays false:
@@ -3070,6 +3078,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val flags = rawArg ushr 8
playhead.updateTrackerGlobalBehaviour(flags)
}
EffectOp.OP_5 -> applyFilterParamEffect(ts, voice, vi, rawArg, isResonance = false) // 5 $xxyy — Filter Cutoff Control
EffectOp.OP_6 -> applyFilterParamEffect(ts, voice, vi, rawArg, isResonance = true) // 6 $xxyy — Filter Resonance Control
EffectOp.OP_8 -> {
// 8 $xyzz — Bitcrusher. See TAUD_NOTE_EFFECTS.md §8.
// x = clipping mode (shared with effect 9): 0 clamp, 1 fold, 2 wrap.
@@ -3386,24 +3396,37 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
0x4 -> { voice.tremoloWave = x and 3; voice.tremoloRetrig = (x and 4) == 0 }
0x5 -> { voice.panbrelloWave = x and 3; voice.panbrelloRetrig = (x and 4) == 0 }
0x6 -> ts.finePatternDelayExtra += x // fine pattern delay: accumulate across channels
0x7 -> when (x) {
// Past-note actions on the channel's background ghosts.
0x0 -> applyPastNoteAction(ts, vi, 0) // Past Note Cut
0x1 -> applyPastNoteAction(ts, vi, 1) // Past Note Off
0x2 -> applyPastNoteAction(ts, vi, 2) // Past Note Fade
// NNA override for the live note (used at next NNA event on this voice).
// Codes follow the per-voice nnaOverride convention (0=Off, 1=Cut, 2=Continue, 3=Fade).
0x3 -> voice.nnaOverride = 1 // NNA Note Cut
0x4 -> voice.nnaOverride = 2 // NNA Note Continue
0x5 -> voice.nnaOverride = 0 // NNA Note Off
0x6 -> voice.nnaOverride = 3 // NNA Note Fade
// Envelope on/off — mixer ignores and per-tick freezes the disabled envelope.
0x7 -> voice.volEnvOn = false
0x8 -> voice.volEnvOn = true
0x9 -> voice.panEnvOn = false
0xA -> voice.panEnvOn = true
0xB -> voice.pfEnvOn = false
0xC -> voice.pfEnvOn = true
0x7 -> {
// S$7x — Note/Instrument actions (TAUD_NOTE_EFFECTS.md §"S $7x00").
// $0..$6 (past-note actions + NNA override) are no-ops on a metainstrument: its
// live layer-child ghosts would otherwise be mistaken for past notes and culled.
// $7..$E (envelope toggles) fan out across a meta's constituents — the foreground
// voice plus every layer-child ghost on this channel (see [forEachEnvTarget]).
val isMeta = voice.metaForeground
when (x) {
// Past-note actions on the channel's background ghosts.
0x0 -> if (!isMeta) applyPastNoteAction(ts, vi, 0) // Past Note Cut
0x1 -> if (!isMeta) applyPastNoteAction(ts, vi, 1) // Past Note Off
0x2 -> if (!isMeta) applyPastNoteAction(ts, vi, 2) // Past Note Fade
// NNA override for the live note (used at next NNA event on this voice).
// Codes follow the per-voice nnaOverride convention (0=Off, 1=Cut, 2=Continue, 3=Fade).
0x3 -> if (!isMeta) voice.nnaOverride = 1 // NNA Note Cut
0x4 -> if (!isMeta) voice.nnaOverride = 2 // NNA Note Continue
0x5 -> if (!isMeta) voice.nnaOverride = 0 // NNA Note Off
0x6 -> if (!isMeta) voice.nnaOverride = 3 // NNA Note Fade
// Envelope on/off — mixer ignores and per-tick freezes the disabled envelope.
0x7 -> forEachEnvTarget(ts, voice, vi) { it.volEnvOn = false } // Volume Env Off
0x8 -> forEachEnvTarget(ts, voice, vi) { it.volEnvOn = true } // Volume Env On
0x9 -> forEachEnvTarget(ts, voice, vi) { it.panEnvOn = false } // Panning Env Off
0xA -> forEachEnvTarget(ts, voice, vi) { it.panEnvOn = true } // Panning Env On
// $B/$C target the PITCH envelope when one is defined; on a filter-only
// instrument they fall back to the filter env (IT "pitch or filter" semantics).
0xB -> forEachEnvTarget(ts, voice, vi) { if (it.hasPitchEnv) it.pitchEnvOn = false else if (it.hasFilterEnv) it.filterEnvOn = false }
0xC -> forEachEnvTarget(ts, voice, vi) { if (it.hasPitchEnv) it.pitchEnvOn = true else if (it.hasFilterEnv) it.filterEnvOn = true }
// $D/$E toggle the FILTER envelope specifically (Taud-specific; differs from MPTM).
0xD -> forEachEnvTarget(ts, voice, vi) { it.filterEnvOn = false } // Filter Env Off
0xE -> forEachEnvTarget(ts, voice, vi) { it.filterEnvOn = true } // Filter Env On
}
}
0x8 -> {
// S$80xx — full 8-bit pan; arg low byte is the value.
@@ -3439,6 +3462,66 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
}
/** Apply an envelope toggle (S$77..$7E) to the right voice set: the foreground voice plus —
* for a metainstrument — every layer-child ghost on this channel, so all constituents move
* together. Ordinary instruments have no layer children, so only the foreground voice is
* touched. See TAUD_NOTE_EFFECTS.md §"S $7x00". */
private inline fun forEachEnvTarget(ts: TrackerState, voice: Voice, vi: Int, action: (Voice) -> Unit) {
action(voice)
for (bg in ts.backgroundVoices) if (bg.isLayerChild && bg.sourceChannel == vi) action(bg)
}
/**
* notefx 5 (cutoff) / 6 (resonance) — Filter Cutoff/Resonance Control (TAUD_NOTE_EFFECTS.md §"5/6").
*
* Sets the instrument's filter cutoff (5) or resonance (6) directly; the change is instrument-wide,
* so every note that shares the instrument — including notes already sounding — is affected. The
* value is read mode-aware: IT mode takes the high byte ($xx) only, SF mode takes the full 16-bit
* argument ($xxyy). $FFFF clears the override and restores the instrument's loaded default. The
* effect has no memory.
*
* On a metainstrument the change fans out across every constituent currently sounding on this
* channel (the foreground layer 0 plus its layer-child ghosts), so the whole stack moves together.
*/
private fun applyFilterParamEffect(ts: TrackerState, voice: Voice, vi: Int, rawArg: Int, isResonance: Boolean) {
// Target instrument set: the foreground voice's instrument plus those of any layer-child
// ghosts on this channel (the set is just the one instrument for an ordinary instrument).
val targets = HashSet<Int>()
targets.add(voice.instrumentId)
for (bg in ts.backgroundVoices) if (bg.isLayerChild && bg.sourceChannel == vi) targets.add(bg.instrumentId)
for (id in targets) {
val ti = instruments[id]
val value = when {
rawArg == 0xFFFF -> -1 // reset: drop the override, restore default
ti.filterSfMode -> rawArg and 0xFFFF // SF mode: full 16-bit cents / centibels
else -> (rawArg ushr 8) and 0xFF // IT mode: high byte only
}
if (isResonance) ti.resonanceOverride = value else ti.cutoffOverride = value
}
// Push the resolved value into every currently-active voice that shares a target instrument
// so notes already sounding change immediately. Voices with a filter envelope recompute
// currentCutoff from activeDefaultCutoff each tick; voices without one (and resonance, which
// has no per-tick recompute) read the value seeded here. filterSfMode is re-synced so the
// per-tick filter math reads the value in the right units.
fun push(v: Voice) {
if (v.instrumentId !in targets) return
val ti = instruments[v.instrumentId]
v.filterSfMode = ti.filterSfMode
if (isResonance) {
v.activeDefaultResonance = ti.defaultResonance16
v.currentResonance = v.activeDefaultResonance
} else {
v.activeDefaultCutoff = ti.defaultCutoff16
v.currentCutoff = v.activeDefaultCutoff
}
v.filterCutoffCached = -1; v.filterResonanceCached = -1 // force coefficient refresh
}
for (v in ts.voices) if (v.active) push(v)
for (bg in ts.backgroundVoices) if (bg.active) push(bg)
}
private fun applyTrackerTick(ts: TrackerState, playhead: Playhead) {
val tickSec = 2.5 / playhead.bpm
// Samples-per-tick at the current BPM — used to spread the per-tick envVolume
@@ -3649,7 +3732,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// IT pitch envelope max is ±16 semitones (Schism sndmix.c:455-462 indexes
// linear_slide_up_table[abs(envpitch)] where envpitch ∈ [-256,+256] and
// table[255] = 65536·2^(255/192) ≈ 2.504, i.e. 15.94 semitones).
val pitchEnvDelta = if (voice.hasPitchEnv && voice.pfEnvOn)
val pitchEnvDelta = if (voice.hasPitchEnv && voice.pitchEnvOn)
((voice.envPitchValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
else 0
@@ -3665,7 +3748,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// If the instrument has no initial cutoff (255 = off), the envelope drives the filter
// from the maximum active value (254) so the filter can become audible during the note.
// baseCut is the ACTIVE cutoff (patch 'x' override or base inst).
if (voice.hasFilterEnv && voice.pfEnvOn) {
if (voice.hasFilterEnv && voice.filterEnvOn) {
if (voice.filterSfMode) {
// SF mode: activeDefaultCutoff is the PEAK cutoff in cents; the env scales it
// down (envFilterValue 1.0 = peak/open, 0 = closed). Converter sets node values
@@ -3802,7 +3885,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
// Auto-vibrato keeps running on backgrounds — it's an instrument-intrinsic LFO.
val autoVibDelta = advanceAutoVibrato(bg, inst)
val pitchEnvDelta = if (bg.hasPitchEnv && bg.pfEnvOn)
val pitchEnvDelta = if (bg.hasPitchEnv && bg.pitchEnvOn)
((bg.envPitchValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
else 0
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
@@ -3810,7 +3893,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Filter envelope: same scaling rule as foreground, using the active cutoff.
// Must branch on SF mode too — an SF-mode ghost's cutoff is in cents (0..0xFFFF),
// so the IT 0..254 clamp would otherwise collapse it to ~9 Hz (total muffling).
if (bg.hasFilterEnv && bg.pfEnvOn) {
if (bg.hasFilterEnv && bg.filterEnvOn) {
if (bg.filterSfMode) {
val baseCut = if (bg.activeDefaultCutoff < 0xFFFF) bg.activeDefaultCutoff else 13500
bg.currentCutoff = (baseCut * bg.envFilterValue).toInt().coerceIn(0, 0xFFFF)
@@ -4253,11 +4336,17 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// -1 = use instrument-default NNA; otherwise overrides the next NNA event on this voice
// (see S $73..$76). Cleared on every fresh trigger.
var nnaOverride = -1
// Per-voice envelope gates (S $77..$7C). When false the corresponding envelope is frozen
// *and* its value is treated as unity by the mixer / pitch path.
// Per-voice envelope gates (S $77..$7E). When false the corresponding envelope is frozen
// *and* its value is treated as unity by the mixer / pitch path. The pitch and filter
// gates are independent so S$7B/$7C (pitch) and S$7D/$7E (filter) toggle them separately.
var volEnvOn = true
var panEnvOn = true
var pfEnvOn = true
var pitchEnvOn = true
var filterEnvOn = true
// True when this foreground voice was triggered as a metainstrument's layer 0. Drives the
// S$70..$76 no-op and the S$77..$7E / notefx-5/6 fan-out across constituent layers. Always
// false on ordinary-instrument voices and on layer-child ghosts. See TAUD_NOTE_EFFECTS.md S$7x.
var metaForeground = false
// Note-Fade NNA flag — triggers volume fadeout without sustain release (vs keyOff which
// also breaks the volume envelope's sustain loop). Both paths feed the same fade decay.
var noteFading = false
@@ -4776,7 +4865,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
it.funkWritePos = 0
it.fader = 0
it.nnaOverride = -1
it.volEnvOn = true; it.panEnvOn = true; it.pfEnvOn = true
it.volEnvOn = true; it.panEnvOn = true; it.pitchEnvOn = true; it.filterEnvOn = true
it.metaForeground = false
it.noteFading = false
it.layerMixGain = 1.0; it.isLayerChild = false; it.layerRelDetune = 0
// "What's playing" state — must be cleared alongside the volume reset
@@ -4818,7 +4908,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// within a single playback (matching PT2's destructive-but-stable behaviour);
// here we snapshot back to "no inversions yet" so a fresh play is reproducible
// without needing to reload the song from disk.
parent.instruments.forEach { it.funkMask = null }
// notefx 5/6 cutoff/resonance overrides are likewise per-instrument runtime
// state — clear them so a replay (or song loop) starts from the file defaults.
parent.instruments.forEach { it.funkMask = null; it.cutoffOverride = -1; it.resonanceOverride = -1 }
}
}
@@ -5107,14 +5199,25 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
* (8-bit cutoff/resonance in bytes 182/183), true = SoundFont (16-bit: cutoff cents in
* byte 182<<8|252, resonance centibels in byte 183<<8|253). See [refreshVoiceFilter]. */
val filterSfMode: Boolean get() = (fadeoutHigh ushr 4) and 1 != 0
// Runtime cutoff / resonance overrides set by notefx 5 / 6 (Filter Cutoff/Resonance
// Control, TAUD_NOTE_EFFECTS.md §"5/6"). -1 = no override (use the loaded default).
// Stored in the active filter mode's native units (IT: 8-bit byte; SF: 16-bit cents /
// centibels) so the *16 getters can return them verbatim. notefx 5/6 $FFFF clears the
// override back to -1, restoring the loaded default. The effect is instrument-wide: every
// note that shares this instrument reads these through [defaultCutoff16]/[defaultResonance16].
var cutoffOverride: Int = -1
var resonanceOverride: Int = -1
/** Default cutoff resolved for the active filter mode: 8-bit IT byte, or the 16-bit
* SF absolute-cents value (high byte 182, low byte 252). */
* SF absolute-cents value (high byte 182, low byte 252). A notefx-5 override wins. */
val defaultCutoff16: Int get() =
if (filterSfMode) ((defaultCutoff and 0xFF) shl 8) or (reserved[1].toInt() and 0xFF) else defaultCutoff
if (cutoffOverride >= 0) cutoffOverride
else if (filterSfMode) ((defaultCutoff and 0xFF) shl 8) or (reserved[1].toInt() and 0xFF) else defaultCutoff
/** Default resonance resolved for the active filter mode: 8-bit IT byte, or the 16-bit
* SF centibel value (high byte 183, low byte 253). */
* SF centibel value (high byte 183, low byte 253). A notefx-6 override wins. */
val defaultResonance16: Int get() =
if (filterSfMode) ((defaultResonance and 0xFF) shl 8) or (reserved[2].toInt() and 0xFF) else defaultResonance
if (resonanceOverride >= 0) resonanceOverride
else if (filterSfMode) ((defaultResonance and 0xFF) shl 8) or (reserved[2].toInt() and 0xFF) else defaultResonance
// Reserved padding at offsets 251..255 (5 bytes per instrument). Bytes
// 197..250 are now the 2nd pf-envelope (pf2EnvLoop/pf2EnvSustainWord/pf2Envelopes).
@@ -5166,6 +5269,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
* (u32 sample-pointer high 16 bits == 0xFFFF) and parses its layer table;
* otherwise falls back to the per-byte [setByte] field assignment. */
fun loadRecord(b: IntArray) {
// A fresh record replaces any notefx 5/6 cutoff/resonance override from a prior song.
cutoffOverride = -1; resonanceOverride = -1
val sp = (b[0] and 0xFF) or ((b[1] and 0xFF) shl 8) or
((b[2] and 0xFF) shl 16) or ((b[3] and 0xFF) shl 24)
if ((sp ushr 16) and 0xFFFF == 0xFFFF) {