mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
fix: ImpulseTracker style instrument filters
This commit is contained in:
45
it2taud.py
45
it2taud.py
@@ -300,9 +300,13 @@ def _it214_decompress_block(payload: bytes, num_samples: int,
|
|||||||
delta = _sign_extend(v, width)
|
delta = _sign_extend(v, width)
|
||||||
is_data = True
|
is_data = True
|
||||||
elif width < init_width:
|
elif width < init_width:
|
||||||
# Mode B (mid): `range_count` escape codes centred on signed midpoint.
|
# Mode B (mid): `range_count` escape codes occupy values (border, border+range_count].
|
||||||
|
# The encoder simply does NOT emit data values that would collide with this slot —
|
||||||
|
# it widens first. So values *above* the escape range are sign-extended verbatim,
|
||||||
|
# not collapsed. Reference: schismtracker fmt/compression.c:103-127 and
|
||||||
|
# MilkyTracker XModule.cpp:629-640.
|
||||||
# border = (mask >> (init_width-width)) - border_sub, where border_sub
|
# border = (mask >> (init_width-width)) - border_sub, where border_sub
|
||||||
# = range_count / 2. Reference: libxmp it_compress.c, OpenMPT ITTools.cpp.
|
# = range_count / 2.
|
||||||
# 8-bit: width=7 → border=63-4=59, width=8 → border=127-4=123
|
# 8-bit: width=7 → border=63-4=59, width=8 → border=127-4=123
|
||||||
# 16-bit: width=7..16 with border_sub=8.
|
# 16-bit: width=7..16 with border_sub=8.
|
||||||
border = (mask >> (init_width - width)) - border_sub
|
border = (mask >> (init_width - width)) - border_sub
|
||||||
@@ -310,8 +314,6 @@ def _it214_decompress_block(payload: bytes, num_samples: int,
|
|||||||
new_w = v - border
|
new_w = v - border
|
||||||
width = new_w if new_w < width else new_w + 1 # skip-self
|
width = new_w if new_w < width else new_w + 1 # skip-self
|
||||||
continue
|
continue
|
||||||
if v > border + range_count:
|
|
||||||
v -= range_count # collapse escape range out
|
|
||||||
delta = _sign_extend(v, width)
|
delta = _sign_extend(v, width)
|
||||||
is_data = True
|
is_data = True
|
||||||
else:
|
else:
|
||||||
@@ -467,7 +469,8 @@ class ITInstrument:
|
|||||||
__slots__ = ('name', 'dfp', 'gv', 'canonical_sample', 'canonical_volume',
|
__slots__ = ('name', 'dfp', 'gv', 'canonical_sample', 'canonical_volume',
|
||||||
'vol_envelope', 'pan_envelope', 'vol_env_sustain', 'pan_env_sustain',
|
'vol_envelope', 'pan_envelope', 'vol_env_sustain', 'pan_env_sustain',
|
||||||
'pf_envelope', 'pf_env_sustain', 'pf_is_filter',
|
'pf_envelope', 'pf_env_sustain', 'pf_is_filter',
|
||||||
'ifc', 'ifr', 'fadeout', 'pps', 'ppc', 'rv', 'rp', 'nna')
|
'ifc', 'ifr', 'fadeout', 'pps', 'ppc', 'rv', 'rp', 'nna',
|
||||||
|
'dct', 'dca')
|
||||||
# vol_envelope / pan_envelope / pf_envelope: list of 25 (value, minifloat_idx) tuples, or None
|
# vol_envelope / pan_envelope / pf_envelope: list of 25 (value, minifloat_idx) tuples, or None
|
||||||
# *_env_sustain: int (16-bit, 0b 0ut sssss pcb eeeee), 0 = no envelope
|
# *_env_sustain: int (16-bit, 0b 0ut sssss pcb eeeee), 0 = no envelope
|
||||||
# pf_is_filter: bool — pf envelope mode (False = pitch, True = filter)
|
# pf_is_filter: bool — pf envelope mode (False = pitch, True = filter)
|
||||||
@@ -489,6 +492,11 @@ def parse_instruments(data: bytes, h: ITHeader) -> list:
|
|||||||
inst.name = data[ptr+0x20:ptr+0x3A].rstrip(b'\x00').decode('latin-1', errors='replace')
|
inst.name = data[ptr+0x20:ptr+0x3A].rstrip(b'\x00').decode('latin-1', errors='replace')
|
||||||
# NNA at IMPI+0x11 (new format). 0=cut, 1=continue, 2=note off, 3=note fade.
|
# NNA at IMPI+0x11 (new format). 0=cut, 1=continue, 2=note off, 3=note fade.
|
||||||
inst.nna = data[ptr + 0x11] & 0x03
|
inst.nna = data[ptr + 0x11] & 0x03
|
||||||
|
# DCT (Duplicate Check Type) and DCA (Duplicate Check Action), per Schism iti.c:80-94.
|
||||||
|
# DCT: 0=off, 1=note, 2=sample, 3=instrument.
|
||||||
|
# DCA: 0=note cut, 1=note off, 2=note fade.
|
||||||
|
inst.dct = data[ptr + 0x12] & 0x03
|
||||||
|
inst.dca = data[ptr + 0x13] & 0x03
|
||||||
inst.fadeout = struct.unpack_from('<H', data, ptr + 0x14)[0] # 0..1024
|
inst.fadeout = struct.unpack_from('<H', data, ptr + 0x14)[0] # 0..1024
|
||||||
# PPS is signed -32..+32; PPC is the centre note (IT note number 0..119, C-5=60).
|
# PPS is signed -32..+32; PPC is the centre note (IT note number 0..119, C-5=60).
|
||||||
inst.pps = struct.unpack_from('b', data, ptr + 0x16)[0]
|
inst.pps = struct.unpack_from('b', data, ptr + 0x16)[0]
|
||||||
@@ -516,9 +524,12 @@ def parse_instruments(data: bytes, h: ITHeader) -> list:
|
|||||||
inst.canonical_sample = c5_smp # 1-based sample index, 0 = none
|
inst.canonical_sample = c5_smp # 1-based sample index, 0 = none
|
||||||
inst.canonical_volume = min(inst.gv, 64)
|
inst.canonical_volume = min(inst.gv, 64)
|
||||||
|
|
||||||
# Initial filter cutoff/resonance (high bit = enabled, low 7 bits = value)
|
# Initial filter cutoff/resonance (high bit = enabled, low 7 bits = value).
|
||||||
ifc_raw = data[ptr + 0x39]
|
# Per Schism iti.c struct it_instrument: name[26] occupies 0x20..0x39,
|
||||||
ifr_raw = data[ptr + 0x3A]
|
# ifc is at 0x3A, ifr at 0x3B. Off-by-one would silently disable filters
|
||||||
|
# on every IT instrument because name's last byte is always NUL.
|
||||||
|
ifc_raw = data[ptr + 0x3A]
|
||||||
|
ifr_raw = data[ptr + 0x3B]
|
||||||
# Taud uses full 0..255 range (double IT's resolution): IT 0..127 → Taud 0..254,
|
# Taud uses full 0..255 range (double IT's resolution): IT 0..127 → Taud 0..254,
|
||||||
# IT "off" (high bit clear) → Taud 255.
|
# IT "off" (high bit clear) → Taud 255.
|
||||||
inst.ifc = (ifc_raw & 0x7F) * 2 if (ifc_raw & 0x80) else 255
|
inst.ifc = (ifc_raw & 0x7F) * 2 if (ifc_raw & 0x80) else 255
|
||||||
@@ -1114,7 +1125,7 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
vol_env, vol_sus, pan_env, pan_sus, pf_env, pf_sus, pf_is_filter,
|
vol_env, vol_sus, pan_env, pan_sus, pf_env, pf_sus, pf_is_filter,
|
||||||
inst_gv, fadeout, vib_speed, vib_depth, vib_sweep, vib_rate, vib_wave,
|
inst_gv, fadeout, vib_speed, vib_depth, vib_sweep, vib_rate, vib_wave,
|
||||||
default_pan, pps, ppc_taud, pan_swing, vol_swing, ifc, ifr,
|
default_pan, pps, ppc_taud, pan_swing, vol_swing, ifc, ifr,
|
||||||
sample_detune, nna.
|
sample_detune, nna, dct, dca.
|
||||||
All optional; missing keys default to neutral values.
|
All optional; missing keys default to neutral values.
|
||||||
|
|
||||||
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict).
|
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict).
|
||||||
@@ -1282,7 +1293,13 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
inst_bin[base + 187] = idata.get('vib_depth', 0) & 0xFF
|
inst_bin[base + 187] = idata.get('vib_depth', 0) & 0xFF
|
||||||
# Byte 188: vibrato rate (0..255 full range, IT samplewise Vir).
|
# Byte 188: vibrato rate (0..255 full range, IT samplewise Vir).
|
||||||
inst_bin[base + 188] = idata.get('vib_rate', 0) & 0xFF
|
inst_bin[base + 188] = idata.get('vib_rate', 0) & 0xFF
|
||||||
# Bytes 189-191: reserved (already zeroed).
|
# Byte 189: duplicate-check / action (IT-only — bits 0-1 = DCT, bits 2-3 = DCA).
|
||||||
|
# DCT: 0=off, 1=note, 2=sample, 3=instrument.
|
||||||
|
# DCA: 0=note cut, 1=note off, 2=note fade.
|
||||||
|
dct = idata.get('dct', 0) & 0x03
|
||||||
|
dca = idata.get('dca', 0) & 0x03
|
||||||
|
inst_bin[base + 189] = (dca << 2) | dct
|
||||||
|
# Bytes 190-191: reserved (already zeroed).
|
||||||
|
|
||||||
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
|
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
|
||||||
|
|
||||||
@@ -1622,14 +1639,14 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
default_pan = 0x80
|
default_pan = 0x80
|
||||||
|
|
||||||
# Auto-vibrato lives on the canonical sample (not the IT instrument).
|
# Auto-vibrato lives on the canonical sample (not the IT instrument).
|
||||||
# IT samplewise auto-vibrato: Vis (speed 0..64), Vid (depth 0..32),
|
# IT samplewise auto-vibrato: Vis (speed 0..64), Vid (depth 0..64),
|
||||||
# Vir (rate 0..255 — IT-style ramp-in), Vit (waveform 0..3).
|
# Vir (rate 0..255 — IT-style ramp-in), Vit (waveform 0..3).
|
||||||
# Taud byte 175 (Vibrato Speed) follows FT2 0..255 scale: rescale Vis.
|
# Taud byte 175 (Vibrato Speed) follows FT2 0..255 scale: rescale Vis.
|
||||||
# Taud byte 187 (Vibrato Depth) is full 0..255: rescale Vid 0..32 → 0..255.
|
# Taud byte 187 (Vibrato Depth) is full 0..255: rescale Vid 0..64 → 0..255.
|
||||||
# Taud byte 188 (Vibrato Rate) is IT Vir verbatim.
|
# Taud byte 188 (Vibrato Rate) is IT Vir verbatim.
|
||||||
# Taud byte 176 (Vibrato Sweep) is FT2-only — leave 0 for IT.
|
# Taud byte 176 (Vibrato Sweep) is FT2-only — leave 0 for IT.
|
||||||
vib_speed_taud = min(255, round(src_smp.av_speed * 255 / 64))
|
vib_speed_taud = min(255, round(src_smp.av_speed * 255 / 64))
|
||||||
vib_depth_taud = min(255, round(src_smp.av_depth * 255 / 32))
|
vib_depth_taud = min(255, round(src_smp.av_depth * 255 / 64))
|
||||||
# IT NNA (0=cut, 1=continue, 2=note off, 3=note fade) →
|
# IT NNA (0=cut, 1=continue, 2=note off, 3=note fade) →
|
||||||
# Taud NNA (00=note off, 01=cut, 10=continue, 11=fade).
|
# Taud NNA (00=note off, 01=cut, 10=continue, 11=fade).
|
||||||
it_to_taud_nna = (0b01, 0b10, 0b00, 0b11)
|
it_to_taud_nna = (0b01, 0b10, 0b00, 0b11)
|
||||||
@@ -1658,6 +1675,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
'ifr': inst.ifr,
|
'ifr': inst.ifr,
|
||||||
'sample_detune': 0, # IT samples have no finetune
|
'sample_detune': 0, # IT samples have no finetune
|
||||||
'nna': nna_taud,
|
'nna': nna_taud,
|
||||||
|
'dct': inst.dct,
|
||||||
|
'dca': inst.dca,
|
||||||
}
|
}
|
||||||
sampleinst_raw, _ = build_sample_inst_bin_it(proxy, instr_data_by_slot)
|
sampleinst_raw, _ = build_sample_inst_bin_it(proxy, instr_data_by_slot)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2108,6 +2108,10 @@ TODO:
|
|||||||
[x] cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
[x] cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
||||||
[x] figure out how IT (0..256) and FT2 (0..FFF + cut) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
[x] figure out how IT (0..256) and FT2 (0..FFF + cut) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
||||||
[x] Pitchbend on Amiga frequency mode sometimes works right, sometimes works wrong. (effect underdelivers) Affects every song with Amiga picth mode, AND ON THE fresh taut.js session only
|
[x] Pitchbend on Amiga frequency mode sometimes works right, sometimes works wrong. (effect underdelivers) Affects every song with Amiga picth mode, AND ON THE fresh taut.js session only
|
||||||
|
[x] Fix 4THSYM.it filters
|
||||||
|
[ ] 4THSYM.it: pitchbend is wrong, some notes keep playing (loudly!) even if new notes are emitted
|
||||||
|
[ ] some notes are emitted with wrong volset (tested with .mod, may affect others as well)
|
||||||
|
[ ] nearly_there_.mod: `C#5 SD300 / ... / C-5 SD200 / A#4 / G#4`: every C-5 SD200 (there are four occurances) gets skipped
|
||||||
[ ] implement bitcrusher and overdrive (eff sym '8' and '9')
|
[ ] implement bitcrusher and overdrive (eff sym '8' and '9')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1570,6 +1570,63 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
if (voice.panbrelloRetrig) voice.panbrelloLfoPos = 0
|
if (voice.panbrelloRetrig) voice.panbrelloLfoPos = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IT-style Duplicate Check (DCT/DCA). Runs *before* NNA on every fresh foreground
|
||||||
|
* trigger: existing voices on this channel — the foreground itself plus any of its
|
||||||
|
* own background ghosts — that match the new note under DCT have DCA applied.
|
||||||
|
* Reference: schismtracker effects.c:1664-1764 (csf_check_nna).
|
||||||
|
*
|
||||||
|
* DCT (per existing voice's instrument):
|
||||||
|
* 1 = note — same noteVal AND same instrumentId
|
||||||
|
* 2 = sample — same canonical sample (matched by samplePtr+sampleLength)
|
||||||
|
* 3 = instrument — same instrumentId
|
||||||
|
* DCA: 0 = note cut, 1 = note off (release sustain), 2 = note fade.
|
||||||
|
*
|
||||||
|
* Note: the foreground voice will be replaced by triggerNote() right after this,
|
||||||
|
* so applying DCA to it is mostly relevant for ghosts spawned *from* it via NNA
|
||||||
|
* — the ghost is cloned from the (already-DCA-modified) foreground state.
|
||||||
|
*/
|
||||||
|
private fun applyDuplicateCheck(ts: TrackerState, channel: Int, newInstId: Int, newNote: Int) {
|
||||||
|
if (newInstId == 0) return
|
||||||
|
val newInst = instruments[newInstId]
|
||||||
|
|
||||||
|
fun isDuplicate(v: Voice): Boolean {
|
||||||
|
val existInst = instruments[v.instrumentId]
|
||||||
|
return when (existInst.duplicateCheckType) {
|
||||||
|
1 -> v.noteVal == newNote && v.instrumentId == newInstId
|
||||||
|
2 -> v.instrumentId == newInstId &&
|
||||||
|
existInst.samplePtr == newInst.samplePtr &&
|
||||||
|
existInst.sampleLength == newInst.sampleLength
|
||||||
|
3 -> v.instrumentId == newInstId
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyAction(v: Voice) {
|
||||||
|
val existInst = instruments[v.instrumentId]
|
||||||
|
when (existInst.duplicateCheckAction) {
|
||||||
|
0 -> { v.fadeoutVolume = 0.0; v.active = false }
|
||||||
|
1 -> v.keyOff = true
|
||||||
|
2 -> v.noteFading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fg = ts.voices[channel]
|
||||||
|
if (fg.active && instruments[fg.instrumentId].duplicateCheckType != 0 && isDuplicate(fg)) {
|
||||||
|
applyAction(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
val it = ts.backgroundVoices.iterator()
|
||||||
|
while (it.hasNext()) {
|
||||||
|
val bg = it.next()
|
||||||
|
if (bg.sourceChannel != channel || !bg.active) continue
|
||||||
|
if (instruments[bg.instrumentId].duplicateCheckType == 0) continue
|
||||||
|
if (!isDuplicate(bg)) continue
|
||||||
|
applyAction(bg)
|
||||||
|
if (!bg.active) it.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On a fresh foreground trigger, optionally migrate the existing voice into the
|
* On a fresh foreground trigger, optionally migrate the existing voice into the
|
||||||
* mixer-private background pool per the New Note Action setting (instrument default
|
* mixer-private background pool per the New Note Action setting (instrument default
|
||||||
@@ -1751,6 +1808,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
voice.delayedInst = row.instrment
|
voice.delayedInst = row.instrment
|
||||||
voice.delayedVol = if (row.volume >= 0) row.volume else -1
|
voice.delayedVol = if (row.volume >= 0) row.volume else -1
|
||||||
} else {
|
} else {
|
||||||
|
applyDuplicateCheck(ts, vi, row.instrment, row.note)
|
||||||
maybeSpawnBackgroundForNNA(ts, voice, vi)
|
maybeSpawnBackgroundForNNA(ts, voice, vi)
|
||||||
triggerNote(voice, row.note, row.instrment, -1)
|
triggerNote(voice, row.note, row.instrment, -1)
|
||||||
}
|
}
|
||||||
@@ -2027,6 +2085,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// Note delay — fire deferred trigger when the requested tick arrives.
|
// Note delay — fire deferred trigger when the requested tick arrives.
|
||||||
// NNA fires now (not at row parse) so that delayed retriggers ghost correctly.
|
// NNA fires now (not at row parse) so that delayed retriggers ghost correctly.
|
||||||
if (voice.noteDelayTick == ts.tickInRow) {
|
if (voice.noteDelayTick == ts.tickInRow) {
|
||||||
|
applyDuplicateCheck(ts, vi, voice.delayedInst, voice.delayedNote)
|
||||||
maybeSpawnBackgroundForNNA(ts, voice, vi)
|
maybeSpawnBackgroundForNNA(ts, voice, vi)
|
||||||
triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol)
|
triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol)
|
||||||
voice.noteDelayTick = -1
|
voice.noteDelayTick = -1
|
||||||
@@ -2161,12 +2220,17 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE)
|
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE)
|
||||||
voice.playbackRate = computePlaybackRate(inst, finalPitch)
|
voice.playbackRate = computePlaybackRate(inst, finalPitch)
|
||||||
|
|
||||||
// Filter envelope (filter mode): scale current cutoff by env value (0..1, 0.5 = unity).
|
// Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
|
||||||
|
// Schism filters.c:80-86 computes `cutoff_used = chan->cutoff * (flt_modifier+256)/256`
|
||||||
|
// where flt_modifier = (env_value_0..64 - 32) * 8. Mapping TSVM's [0..1] env to Schism's
|
||||||
|
// [-256..+256] modifier and accounting for our pre-doubled defaultCutoff (it2taud.py
|
||||||
|
// stores IFC*2 in 0..254) gives `currentCutoff = baseCut * envPfValue` — at unity (0.5)
|
||||||
|
// the filter sits at IFC, at max (1.0) it opens to 2*IFC, at min (0.0) it closes.
|
||||||
// If the instrument has no initial cutoff (255 = off), the envelope drives the filter
|
// 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.
|
// from the maximum active value (254) so the filter can become audible during the note.
|
||||||
if (voice.hasPfEnv && voice.pfEnvOn && voice.envPfIsFilter) {
|
if (voice.hasPfEnv && voice.pfEnvOn && voice.envPfIsFilter) {
|
||||||
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
|
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
|
||||||
voice.currentCutoff = (baseCut * (voice.envPfValue * 2.0)).toInt().coerceIn(0, 254)
|
voice.currentCutoff = (baseCut * voice.envPfValue).toInt().coerceIn(0, 254)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh biquad filter coefficients once per tick (only recomputes when changed).
|
// Refresh biquad filter coefficients once per tick (only recomputes when changed).
|
||||||
@@ -2250,7 +2314,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// 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) {
|
||||||
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
|
val baseCut = if (inst.defaultCutoff < 255) inst.defaultCutoff else 254
|
||||||
bg.currentCutoff = (baseCut * (bg.envPfValue * 2.0)).toInt().coerceIn(0, 254)
|
bg.currentCutoff = (baseCut * bg.envPfValue).toInt().coerceIn(0, 254)
|
||||||
}
|
}
|
||||||
refreshVoiceFilter(bg)
|
refreshVoiceFilter(bg)
|
||||||
// Reap fully-faded ghosts so the pool stays drained.
|
// Reap fully-faded ghosts so the pool stays drained.
|
||||||
@@ -2969,7 +3033,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
* waveform: 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2)
|
* waveform: 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2)
|
||||||
* 187 u8 vibrato depth (0..255 full range)
|
* 187 u8 vibrato depth (0..255 full range)
|
||||||
* 188 u8 vibrato rate (0..255 full range — IT samplewise Vir)
|
* 188 u8 vibrato rate (0..255 full range — IT samplewise Vir)
|
||||||
* 189..191 byte[3] reserved
|
* 189 u8 duplicate-check / action (IT-only — 0b 0000 aadd)
|
||||||
|
* dd = DCT (Duplicate Check Type) 0=off, 1=note, 2=sample, 3=instrument
|
||||||
|
* aa = DCA (Duplicate Check Action) 0=note cut, 1=note off, 2=note fade
|
||||||
|
* 190..191 byte[2] reserved
|
||||||
*/
|
*/
|
||||||
data class TaudInst(
|
data class TaudInst(
|
||||||
var index: Int,
|
var index: Int,
|
||||||
@@ -3002,7 +3069,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var sampleDetune: Int, // bytes 184-185 (signed 4096-TET stored as u16)
|
var sampleDetune: Int, // bytes 184-185 (signed 4096-TET stored as u16)
|
||||||
var instrumentFlag: Int, // byte 186 (NNA + vibrato waveform)
|
var instrumentFlag: Int, // byte 186 (NNA + vibrato waveform)
|
||||||
var vibratoDepth: Int, // byte 187 (0..255 full range)
|
var vibratoDepth: Int, // byte 187 (0..255 full range)
|
||||||
var vibratoRate: Int // byte 188 (IT samplewise Vir)
|
var vibratoRate: Int, // byte 188 (IT samplewise Vir)
|
||||||
|
var dupCheckFlag: Int // byte 189 (DCT bits 0-1, DCA bits 2-3)
|
||||||
) {
|
) {
|
||||||
constructor(index: Int) : this(
|
constructor(index: Int) : this(
|
||||||
index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
|
index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
|
||||||
@@ -3010,7 +3078,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
|
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
|
||||||
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
|
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
|
||||||
0, 0, 0, 0, 0, 0x80, 0x5000, 0, 0, 0xFF, 0,
|
0, 0, 0, 0, 0, 0x80, 0x5000, 0, 0, 0xFF, 0,
|
||||||
0, 0, 0, 0
|
0, 0, 0, 0, 0
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Sample-flag byte 14 bit 2 — when set, the sample loop is a sustain loop:
|
/** Sample-flag byte 14 bit 2 — when set, the sample loop is a sustain loop:
|
||||||
@@ -3024,9 +3092,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val vibratoWaveform: Int get() = (instrumentFlag ushr 2) and 0x07
|
val vibratoWaveform: Int get() = (instrumentFlag ushr 2) and 0x07
|
||||||
/** Sample detune as a signed 4096-TET delta. */
|
/** Sample detune as a signed 4096-TET delta. */
|
||||||
val sampleDetuneSigned: Int get() = sampleDetune.toShort().toInt()
|
val sampleDetuneSigned: Int get() = sampleDetune.toShort().toInt()
|
||||||
|
/** Duplicate Check Type — 0=off, 1=note, 2=sample, 3=instrument (IT semantics). */
|
||||||
|
val duplicateCheckType: Int get() = dupCheckFlag and 0x03
|
||||||
|
/** Duplicate Check Action — 0=note cut, 1=note off, 2=note fade. */
|
||||||
|
val duplicateCheckAction: Int get() = (dupCheckFlag ushr 2) and 0x03
|
||||||
|
|
||||||
// Reserved padding at offsets 189..191 (3 bytes per instrument).
|
// Reserved padding at offsets 190..191 (2 bytes per instrument).
|
||||||
private val reserved = ByteArray(3)
|
private val reserved = ByteArray(2)
|
||||||
|
|
||||||
// Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region.
|
// Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region.
|
||||||
// Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact.
|
// Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact.
|
||||||
@@ -3108,7 +3180,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
186 -> instrumentFlag.toByte()
|
186 -> instrumentFlag.toByte()
|
||||||
187 -> vibratoDepth.toByte()
|
187 -> vibratoDepth.toByte()
|
||||||
188 -> vibratoRate.toByte()
|
188 -> vibratoRate.toByte()
|
||||||
in 189..191 -> reserved[offset - 189]
|
189 -> dupCheckFlag.toByte()
|
||||||
|
in 190..191 -> reserved[offset - 190]
|
||||||
else -> throw InternalError("Bad offset $offset")
|
else -> throw InternalError("Bad offset $offset")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3163,7 +3236,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
186 -> { instrumentFlag = byte and 0xFF }
|
186 -> { instrumentFlag = byte and 0xFF }
|
||||||
187 -> { vibratoDepth = byte and 0xFF }
|
187 -> { vibratoDepth = byte and 0xFF }
|
||||||
188 -> { vibratoRate = byte and 0xFF }
|
188 -> { vibratoRate = byte and 0xFF }
|
||||||
in 189..191 -> { reserved[offset - 189] = byte.toByte() }
|
189 -> { dupCheckFlag = byte and 0x0F } // DCT (bits 0-1) + DCA (bits 2-3)
|
||||||
|
in 190..191 -> { reserved[offset - 190] = byte.toByte() }
|
||||||
else -> throw InternalError("Bad offset $offset")
|
else -> throw InternalError("Bad offset $offset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user