mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
IT instrument shenanigans
This commit is contained in:
@@ -578,11 +578,11 @@ Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retr
|
||||
|
||||
S is a multiplexing opcode; the **high nibble of the high byte** selects the sub-effect, and the remainder is the sub-argument.
|
||||
|
||||
## S $1x00 — ST3/IT Glissando control
|
||||
## S $1x00 — PT/ST3/IT Glissando control
|
||||
|
||||
**Plain.** `$1000` turns glissando off; `$1100` turns it on. When on, tone portamento (G) output is quantised to the nearest semitone ($0155 approximation) before being sent to the mixer. The internal G pitch counter still advances smoothly; only the audible pitch steps. **This command is implemented sorely for ST3/IT compatibility.** and therefore only works in 12-TET context.
|
||||
**Plain.** `$1000` turns glissando off; `$1100` turns it on. When on, tone portamento (G) output is quantised to the nearest semitone ($0155 approximation) before being sent to the mixer. The internal G pitch counter still advances smoothly; only the audible pitch steps. **This command is implemented sorely for ST3/IT compatibility** and therefore only works in 12-TET context.
|
||||
|
||||
**Compatibility.** ST3 `S10`/`S11` maps directly. In Taud, "nearest semitone" uses the best integer approximation: round `pitch / $155` to the nearest integer, multiply by $155; equivalently, `snapped = (pitch + $AB) / $155 × $155`. Because $155 is an approximation of 4096/12, accumulated rounding across many octaves will drift by up to a few cents; this is documented behaviour and intentional given the microtonal grid.
|
||||
**Compatibility.** ST3/IT `S10`/`S11` and PT `E30`/`E31` maps directly. In Taud, "nearest semitone" uses the best integer approximation: round `pitch / $155` to the nearest integer, multiply by $155; equivalently, `snapped = (pitch + $AB) / $155 × $155`. Because $155 is an approximation of 4096/12, accumulated rounding across many octaves will drift by up to a few cents; this is documented behaviour and intentional given the microtonal grid.
|
||||
|
||||
**Implementation.** Maintain a per-channel boolean `glissando_on`. When G updates `pitch`, if `glissando_on` is set, compute `display_pitch = round(pitch × 12 / 4096) × 4096 / 12` (using integer division with rounding) and send `display_pitch` to the mixer; otherwise send `pitch` directly.
|
||||
|
||||
@@ -634,7 +634,7 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
|
||||
| $6 | Square | No |
|
||||
| $7 | Random | No |
|
||||
|
||||
**Compatibility.** ST3 `S3x` maps directly.
|
||||
**Compatibility.** ST3 `S3x` and ProTracker `E4x` maps directly.
|
||||
|
||||
**Implementation.** Store `vibrato_waveform = $x & $3` and `vibrato_retrigger = (($x & $4) == 0)` for the channel. The ramp-down shape is `$7F − ((pos & $3F) << 2)` across one logical cycle; the square shape is `sign(sine(pos)) × $7F`; random draws a fresh `rand() & $FF − $80` every tick. On a new note, if `vibrato_retrigger` is true, reset `lfo_pos = 0`.
|
||||
|
||||
@@ -644,7 +644,7 @@ ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
|
||||
|
||||
**Plain.** Selects the shape of the tremolo (R) oscillator; value encoding is identical to S $3x.
|
||||
|
||||
**Compatibility.** ST3 `S4x` maps directly. ProTracker `E7x` maps to Taud `S $4x00`.
|
||||
**Compatibility.** ST3 `S4x` and ProTracker `E7x` maps directly.
|
||||
|
||||
**Implementation.** As for S $3x, but applied to R's separate state (`tremolo_waveform`, `tremolo_retrigger`, and tremolo `lfo_pos`).
|
||||
|
||||
|
||||
83
it2taud.py
83
it2taud.py
@@ -458,7 +458,7 @@ class ITInstrument:
|
||||
__slots__ = ('name', 'dfp', 'gv', 'canonical_sample', 'canonical_volume',
|
||||
'vol_envelope', 'pan_envelope', 'vol_env_sustain', 'pan_env_sustain',
|
||||
'pf_envelope', 'pf_env_sustain', 'pf_is_filter',
|
||||
'ifc', 'ifr', 'fadeout', 'pps', 'ppc', 'rv', 'rp')
|
||||
'ifc', 'ifr', 'fadeout', 'pps', 'ppc', 'rv', 'rp', 'nna')
|
||||
# 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
|
||||
# pf_is_filter: bool — pf envelope mode (False = pitch, True = filter)
|
||||
@@ -466,6 +466,7 @@ class ITInstrument:
|
||||
# fadeout : 0..1024 (IT FadeOut field, applied per tick after key-off)
|
||||
# pps / ppc : pitch-pan separation (signed -32..+32) and centre note (0..119)
|
||||
# rv / rp : random volume swing (0..100) / random pan swing (0..64)
|
||||
# nna : new note action (IT 0=cut, 1=continue, 2=note off, 3=note fade)
|
||||
|
||||
def parse_instruments(data: bytes, h: ITHeader) -> list:
|
||||
insts = []
|
||||
@@ -477,6 +478,8 @@ def parse_instruments(data: bytes, h: ITHeader) -> list:
|
||||
|
||||
inst = ITInstrument()
|
||||
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.
|
||||
inst.nna = data[ptr + 0x11] & 0x03
|
||||
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).
|
||||
inst.pps = struct.unpack_from('b', data, ptr + 0x16)[0]
|
||||
@@ -871,8 +874,7 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
||||
if sub == 0x8:
|
||||
# IT S8x: 4-bit → nibble-repeat into 8-bit SEL_SET pan
|
||||
pan8 = (val << 4) | val
|
||||
pan6 = min(0x3F, round(pan8 * 63 / 255))
|
||||
return (TOP_NONE, 0, None, (SEL_SET, pan6))
|
||||
return (TOP_S, 0x8000 | pan8, None, None)
|
||||
if sub == 0x6:
|
||||
vprint(f" dropped S6{val:X} (tick delay) at ch{ch} row{row}")
|
||||
return (TOP_NONE, 0, None, None)
|
||||
@@ -903,10 +905,7 @@ def encode_effect_it(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
||||
return (TOP_NONE, 0, None, None)
|
||||
|
||||
if cmd == EFF_X:
|
||||
# IT X is full 8-bit pan (0=left, 255=right; 128=centre in OpenMPT but
|
||||
# IT spec says 0-255 maps to full left-right)
|
||||
pan6 = min(0x3F, round(arg * 63 / 255))
|
||||
return (TOP_NONE, 0, None, (SEL_SET, pan6))
|
||||
return (TOP_S, 0x8000 | (arg & 0xFF), None, None)
|
||||
|
||||
if cmd == EFF_Y:
|
||||
hi = (arg >> 4) & 0xF
|
||||
@@ -1104,8 +1103,9 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
|
||||
instr_data_by_slot: optional dict mapping taud_slot → dict with keys:
|
||||
vol_env, vol_sus, pan_env, pan_sus, pf_env, pf_sus, pf_is_filter,
|
||||
inst_gv, fadeout, vib_speed, vib_depth, vib_sweep, default_pan,
|
||||
pps, ppc_taud, pan_swing, vol_swing, ifc, ifr.
|
||||
inst_gv, fadeout, vib_speed, vib_depth, vib_sweep, vib_rate, vib_wave,
|
||||
default_pan, pps, ppc_taud, pan_swing, vol_swing, ifc, ifr,
|
||||
sample_detune, nna.
|
||||
All optional; missing keys default to neutral values.
|
||||
|
||||
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict).
|
||||
@@ -1222,10 +1222,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
|
||||
inst_bin[base + 171] = inst_gv & 0xFF
|
||||
inst_bin[base + 172] = fadeout & 0xFF # low 8 bits
|
||||
# Byte 173: high nybble = vibrato depth, low nybble = fadeout high bits.
|
||||
fade_hi = (fadeout >> 8) & 0x03
|
||||
vib_depth = idata.get('vib_depth', 0) & 0x0F
|
||||
inst_bin[base + 173] = ((vib_depth & 0xF) << 4) | fade_hi
|
||||
# Byte 173: low nibble = fadeout high bits (0b 0000 ffff).
|
||||
inst_bin[base + 173] = (fadeout >> 8) & 0x0F
|
||||
inst_bin[base + 174] = idata.get('vol_swing', 0) & 0xFF
|
||||
inst_bin[base + 175] = idata.get('vib_speed', 0) & 0xFF
|
||||
inst_bin[base + 176] = idata.get('vib_sweep', 0) & 0xFF
|
||||
@@ -1238,6 +1236,20 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
inst_bin[base + 181] = idata.get('pan_swing', 0) & 0xFF
|
||||
inst_bin[base + 182] = idata.get('ifc', 255) & 0xFF
|
||||
inst_bin[base + 183] = idata.get('ifr', 255) & 0xFF
|
||||
# Bytes 184-185: sample detune (4096-TET, signed stored as u16).
|
||||
struct.pack_into('<H', inst_bin, base + 184,
|
||||
idata.get('sample_detune', 0) & 0xFFFF)
|
||||
# Byte 186: instrument flag — 0b 000 www nn
|
||||
# nn = NNA (Taud encoding: 00=note off, 01=cut, 10=continue, 11=fade)
|
||||
# www = vibrato waveform (0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up FT2)
|
||||
nna = idata.get('nna', 0) & 0x03
|
||||
vib_wave = idata.get('vib_wave', 0) & 0x07
|
||||
inst_bin[base + 186] = (vib_wave << 2) | nna
|
||||
# Byte 187: vibrato depth (0..255 full range).
|
||||
inst_bin[base + 187] = idata.get('vib_depth', 0) & 0xFF
|
||||
# Byte 188: vibrato rate (0..255 full range, IT samplewise Vir).
|
||||
inst_bin[base + 188] = idata.get('vib_rate', 0) & 0xFF
|
||||
# Bytes 189-191: reserved (already zeroed).
|
||||
|
||||
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
|
||||
|
||||
@@ -1549,23 +1561,48 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
continue
|
||||
src_smp = samples[si]
|
||||
proxy[taud_slot] = src_smp
|
||||
vol64 = min(inst.canonical_volume, 64)
|
||||
inst_vols[taud_slot] = min(vol64, 0x3F)
|
||||
# IT global volume range is 0..128; rescale to Taud's 0..255.
|
||||
inst_gv_255 = min(255, round(inst.gv * 255 / 128))
|
||||
# IT cell-trigger initial volume comes from the sample's default
|
||||
# volume (Dv, 0..64), not the instrument's global volume.
|
||||
smp_default_vol = min(getattr(src_smp, 'vol', 64), 64)
|
||||
inst_vols[taud_slot] = min(smp_default_vol, 0x3F)
|
||||
|
||||
# IT instrument GV (0..128) and sample GV (0..64) collapse into
|
||||
# Taud's single instrumentwise GV (0..255). Sample default volume
|
||||
# is handled separately by inst_vols above.
|
||||
smp_gv = min(getattr(src_smp, 'gv', 64), 64)
|
||||
inst_gv_255 = min(255, round(inst.gv * smp_gv * 255 / (128 * 64)))
|
||||
|
||||
# IT pitch-pan centre: note number 0..119 (C-5 = 60). The Taud
|
||||
# representation is the absolute 4096-TET note value used in patterns
|
||||
# (anchored to TAUD_C4 at IT note 60).
|
||||
ppc_taud = TAUD_C4 + (max(0, min(119, inst.ppc)) - 60) * 4096 // 12
|
||||
|
||||
# IT default pan: 0..64 (centre = 32). Taud uses 0..255.
|
||||
# IT default pan: instrumentwise (IMPI+0x19) takes precedence when
|
||||
# its "use" bit is set; otherwise samplewise (IMPS+0x2F) wins when
|
||||
# its "use" bit is set; otherwise centre (0x80). Both fields encode
|
||||
# 0..64 → rescale to Taud's 0..255 range.
|
||||
smp_dfp_raw = getattr(src_smp, 'dfp', 0)
|
||||
if inst.dfp is not None:
|
||||
default_pan = min(255, max(0, round(inst.dfp * 255 / 64)))
|
||||
elif smp_dfp_raw & 0x80:
|
||||
default_pan = min(255, max(0, round((smp_dfp_raw & 0x7F) * 255 / 64)))
|
||||
else:
|
||||
default_pan = 0x80
|
||||
|
||||
# Auto-vibrato lives on the canonical sample (not the IT instrument).
|
||||
# IT samplewise auto-vibrato: Vis (speed 0..64), Vid (depth 0..32),
|
||||
# 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 187 (Vibrato Depth) is full 0..255: rescale Vid 0..32 → 0..255.
|
||||
# Taud byte 188 (Vibrato Rate) is IT Vir verbatim.
|
||||
# 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_depth_taud = min(255, round(src_smp.av_depth * 255 / 32))
|
||||
# IT NNA (0=cut, 1=continue, 2=note off, 3=note fade) →
|
||||
# Taud NNA (00=note off, 01=cut, 10=continue, 11=fade).
|
||||
it_to_taud_nna = (0b01, 0b10, 0b00, 0b11)
|
||||
nna_taud = it_to_taud_nna[inst.nna & 0x03]
|
||||
|
||||
instr_data_by_slot[taud_slot] = {
|
||||
'vol_env': inst.vol_envelope,
|
||||
'vol_sus': inst.vol_env_sustain,
|
||||
@@ -1575,9 +1612,11 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
'pf_sus': inst.pf_env_sustain,
|
||||
'inst_gv': inst_gv_255,
|
||||
'fadeout': inst.fadeout,
|
||||
'vib_speed': src_smp.av_speed,
|
||||
'vib_depth': src_smp.av_depth,
|
||||
'vib_sweep': src_smp.av_sweep,
|
||||
'vib_speed': vib_speed_taud,
|
||||
'vib_depth': vib_depth_taud,
|
||||
'vib_sweep': 0, # FT2-only; IT uses vib_rate
|
||||
'vib_rate': src_smp.av_sweep & 0xFF, # IT Vir (samplewise sweep)
|
||||
'vib_wave': src_smp.av_wave & 0x07, # IT vib type (0..3)
|
||||
'default_pan': default_pan,
|
||||
'pps': inst.pps,
|
||||
'ppc_taud': ppc_taud & 0xFFFF,
|
||||
@@ -1585,6 +1624,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
'vol_swing': min(255, round(inst.rv * 255 / 100)) if inst.rv else 0,
|
||||
'ifc': inst.ifc,
|
||||
'ifr': inst.ifr,
|
||||
'sample_detune': 0, # IT samples have no finetune
|
||||
'nna': nna_taud,
|
||||
}
|
||||
sampleinst_raw, _ = build_sample_inst_bin_it(proxy, instr_data_by_slot)
|
||||
else:
|
||||
|
||||
@@ -44,7 +44,7 @@ NUM_VOICES = 20
|
||||
NOTE_NOP = 0xFFFF
|
||||
NOTE_KEYOFF = 0x0000
|
||||
NOTE_CUT = 0xFFFE
|
||||
TAUD_C4 = 0x5000 # reference C for instrument sampling rate (was TAUD_C3 = 0x4000)
|
||||
TAUD_C4 = 0x5000 # The audio engine's Middle C
|
||||
|
||||
# Taud effect opcodes (base-36: 0..9 → 0x00..0x09, A..Z → 0x0A..0x23)
|
||||
TOP_NONE = 0x00
|
||||
|
||||
@@ -2053,7 +2053,7 @@ Instrument bin: Registry for 256 instruments, formatted as:
|
||||
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
|
||||
Uint8 Instrument Global Volume (0..255)
|
||||
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
|
||||
- ImpulseTracker has samplewise default volume (0..64) and samplewise global volume (0..64), and they must be taken into account because Taud has no samplewise config, following the ImpulseTracker spec
|
||||
- ImpulseTracker also has samplewise default volume (0..64) and samplewise global volume (0..64), and they must be taken into account because Taud has no samplewise config, following the ImpulseTracker spec
|
||||
* FastTracker2 has range of 0..64; multiply by (255/64) then round to int
|
||||
Uint8 Volume Fadeout low bits (IT: 1..256; XM: 0..255)
|
||||
Bit8 Fadeout and vibrato
|
||||
@@ -2067,7 +2067,7 @@ Instrument bin: Registry for 256 instruments, formatted as:
|
||||
Uint8 Vibrato sweep
|
||||
* FastTracker2 instrument config
|
||||
Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag)
|
||||
* ImpulseTracker has samplewise default volume and samplewise global volume, and they must be taken into account because Taud has no samplewise config
|
||||
* ImpulseTracker has samplewise default pan and instrumentwise default pan, and they must be taken into account because Taud has no samplewise config
|
||||
Uint16 Pitch-pan centre (4096-TET note value)
|
||||
Sint8 Pitch-pan separation (-128..127 full range)
|
||||
Uint8 Pan swing (0..255 full range)
|
||||
@@ -2087,15 +2087,16 @@ Instrument bin: Registry for 256 instruments, formatted as:
|
||||
|
||||
|
||||
TODO:
|
||||
* implement Instrument Flag, Vibrato Depth, Vibrato Rate, other samplewise/instrumentwise changes to it2taud and audio engine
|
||||
* implement new note action on the audio engine (IT uses "background channels", maybe we can do the same but make "background channels" mixer-private)
|
||||
* on playback, panning changes randomly on Taud made by s3m2taud.py and mod2taud.py
|
||||
* implement S6x and S7x command
|
||||
* implement Wxx command (global volume slide)
|
||||
* implement sample loop sustain
|
||||
* Amiga mode freq shift now "underdelivers" (pitch bend not "strong" enough)
|
||||
* cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
||||
* figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
||||
[x] implement Instrument Flag, Vibrato Depth, Vibrato Rate, other samplewise/instrumentwise changes to it2taud and audio engine
|
||||
[ ] implement new note action on the audio engine (IT uses "background channels", maybe we can do the same but make "background channels" mixer-private)
|
||||
[ ] on playback, panning changes randomly on Taud made by s3m2taud.py and mod2taud.py
|
||||
[ ] implement S6x and S7x command
|
||||
[ ] implement Wxx command (global volume slide)
|
||||
[ ] implement sample loop sustain
|
||||
[ ] Amiga mode freq shift now "underdelivers" (pitch bend not "strong" enough)
|
||||
[ ] cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
||||
[ ] figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
||||
[ ] implement bitcrusher (eff sym '8')
|
||||
|
||||
|
||||
Play Data: play data are series of tracker-like instructions, visualised as:
|
||||
|
||||
@@ -1115,11 +1115,6 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
-0x5A, -0x51, -0x47, -0x3C, -0x31, -0x25, -0x19, -0x0C
|
||||
)
|
||||
|
||||
// Funk repeat advance table (S $Fx00). See TAUD_NOTE_EFFECTS.md §S$Fx.
|
||||
private val FUNK_TABLE = intArrayOf(
|
||||
0, 5, 6, 7, 8, 0xA, 0xB, 0xD, 0x10, 0x13, 0x16, 0x1A, 0x20, 0x2B, 0x40, 0x80
|
||||
)
|
||||
|
||||
// ST3-style fine-tune Hz reference offsets in 4096-TET units (S $2x00).
|
||||
private val FINETUNE_OFFSET = intArrayOf(
|
||||
-0x0154, -0x0132, -0x0111, -0x00E4, -0x00B8, -0x008B, -0x005D, -0x003B,
|
||||
@@ -1169,7 +1164,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
}
|
||||
|
||||
private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double =
|
||||
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - MIDDLE_C) / 4096.0)
|
||||
inst.samplingRate.toDouble() / SAMPLING_RATE *
|
||||
2.0.pow((noteVal - MIDDLE_C + inst.sampleDetuneSigned) / 4096.0)
|
||||
|
||||
// Applies one tick of Amiga-mode pitch slide. When the song is in Amiga tone mode, E/F coarse
|
||||
// slide arguments are stored as raw tracker period units (the original ProTracker/ST3 byte),
|
||||
@@ -1396,25 +1392,34 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
* 0 means full depth immediately).
|
||||
*/
|
||||
private fun advanceAutoVibrato(voice: Voice, inst: TaudInst): Int {
|
||||
val depth0 = (inst.fadeoutHighVibDepth ushr 4) and 0xF
|
||||
// Depth from byte 187 (full 0..255). Speed from byte 175 (FT2 0..255 scale).
|
||||
val depth0 = inst.vibratoDepth
|
||||
if (depth0 == 0 || inst.vibratoSpeed == 0) return 0
|
||||
|
||||
val sweep = inst.vibratoSweep
|
||||
val rampDepth = if (sweep == 0) depth0
|
||||
else ((depth0 * voice.autoVibTicksSinceTrigger / sweep)
|
||||
.coerceAtMost(depth0))
|
||||
// Two ramp-in semantics:
|
||||
// FT2 vibratoSweep (byte 176): "ticks to fully ramp" — depth = depth0 * t / sweep.
|
||||
// IT vibratoRate (byte 188): "ramp acceleration" — accumulator += rate per tick,
|
||||
// capped at depth0 * 256, then divided by 256.
|
||||
val ftSweep = inst.vibratoSweep
|
||||
val itRate = inst.vibratoRate
|
||||
val t = voice.autoVibTicksSinceTrigger
|
||||
val rampDepth = when {
|
||||
ftSweep != 0 -> ((depth0 * t / ftSweep).coerceAtMost(depth0))
|
||||
itRate != 0 -> ((t * itRate) ushr 8).coerceAtMost(depth0)
|
||||
else -> depth0
|
||||
}
|
||||
voice.autoVibTicksSinceTrigger++
|
||||
|
||||
// Wave selector lives in the high nibble of vibratoSweep is not standard;
|
||||
// IT keeps a separate wave byte that we don't currently surface, so treat
|
||||
// as sine. The same `lfoSample` table used for H/U effects works here
|
||||
// (8-bit phase, signed -127..+127).
|
||||
val sine = lfoSample(voice.autoVibPhase, 0)
|
||||
// 4096-TET delta: vib depth is in IT units (≈ 1/256 semitone). One
|
||||
// semitone = 4096/12 ≈ 341.33 4096-TET units; IT auto-vibrato depth 1
|
||||
// is ~6.25 cents = 21 4096-TET units. (sine * rampDepth) is roughly
|
||||
// -127*15 .. +127*15 = ±1905, divided by 64 → ±30 ≈ ±10 cents at depth 15.
|
||||
val pitchDelta = (sine * rampDepth) shr 6
|
||||
// Vibrato waveform selector lives in instrumentFlag bits 2-4.
|
||||
// 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2 only).
|
||||
// lfoSample handles 0..3; treat 4 (ramp-up) as negated ramp-down.
|
||||
val wave = inst.vibratoWaveform
|
||||
val rawSample = if (wave == 4) -lfoSample(voice.autoVibPhase, 1)
|
||||
else lfoSample(voice.autoVibPhase, wave and 3)
|
||||
// 4096-TET delta. depth0 is now 0..255 (was 0..15 in old layout); the
|
||||
// shift compensates so depth ≈255 yields a similar musical excursion
|
||||
// (~±9 cents) to the old depth ≈15.
|
||||
val pitchDelta = (rawSample * rampDepth) shr 10
|
||||
voice.autoVibPhase = (voice.autoVibPhase + inst.vibratoSpeed * 2) and 0xFF
|
||||
return pitchDelta
|
||||
}
|
||||
@@ -1821,7 +1826,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
ts.patternDelayRemaining = x
|
||||
}
|
||||
}
|
||||
0xF -> { voice.funkSpeed = x; if (x == 0) voice.funkAccumulator = 0 }
|
||||
0xF -> { voice.funkSpeed = arg and 0xFF; if (x == 0) voice.funkAccumulator = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1983,9 +1988,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
refreshVoiceFilter(voice)
|
||||
|
||||
// Volume fadeout: after key-off, decrement by inst.volumeFadeout / 1024 per tick.
|
||||
// The 10-bit fadeout value is split across volumeFadeoutLow + low nibble of fadeoutHighVibDepth.
|
||||
// The 10-bit fadeout value is split across volumeFadeoutLow + low nibble of fadeoutHigh.
|
||||
if (voice.keyOff) {
|
||||
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHighVibDepth and 0x0F) shl 8)
|
||||
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8)
|
||||
if (fadeStep > 0) {
|
||||
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / 1024.0).coerceAtLeast(0.0)
|
||||
}
|
||||
@@ -2003,12 +2008,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
}
|
||||
}
|
||||
|
||||
// Funk repeat (S$Fx) — advance bit-mask per tick on instruments with active funkSpeed.
|
||||
// Funk repeat (S$Fxxxx) — advance bit-mask per tick on instruments with active funkSpeed.
|
||||
for (voice in ts.voices) {
|
||||
if (voice.funkSpeed == 0 || !voice.active) continue
|
||||
val inst = instruments[voice.instrumentId]
|
||||
if (inst.sampleLoopEnd <= inst.sampleLoopStart) continue
|
||||
voice.funkAccumulator += FUNK_TABLE[voice.funkSpeed and 0xF]
|
||||
voice.funkAccumulator += voice.funkSpeed
|
||||
while (voice.funkAccumulator >= 0x80) {
|
||||
voice.funkAccumulator -= 0x80
|
||||
val loopLen = (inst.sampleLoopEnd - inst.sampleLoopStart).coerceAtLeast(1)
|
||||
@@ -2615,17 +2620,23 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
* 121..170 Bit16×25 pitch/filter envelope points
|
||||
* 171 u8 instrument global volume
|
||||
* 172 u8 volume fadeout low bits
|
||||
* 173 u8 fadeout high (low nibble) + vibrato depth (high nibble)
|
||||
* 173 u8 fadeout high bits (low nibble; 0b 0000 ffff)
|
||||
* 174 u8 volume swing
|
||||
* 175 u8 vibrato speed
|
||||
* 176 u8 vibrato sweep
|
||||
* 175 u8 vibrato speed (FT2 instrumentwise; IT Vis rescaled to 0..255)
|
||||
* 176 u8 vibrato sweep (FT2-only ramp ticks; 0 for IT)
|
||||
* 177 u8 default pan
|
||||
* 178..179 u16 pitch-pan centre (4096-TET)
|
||||
* 180 s8 pitch-pan separation
|
||||
* 181 u8 pan swing
|
||||
* 182 u8 default cutoff
|
||||
* 183 u8 default resonance
|
||||
* 184..191 byte[8] reserved
|
||||
* 184..185 u16 sample detune (4096-TET, signed stored as u16)
|
||||
* 186 u8 instrument flag (0b 000 www nn — NNA bits 0-1, vib waveform bits 2-4)
|
||||
* NNA: 00=note off, 01=note cut, 10=continue, 11=note fade
|
||||
* waveform: 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2)
|
||||
* 187 u8 vibrato depth (0..255 full range)
|
||||
* 188 u8 vibrato rate (0..255 full range — IT samplewise Vir)
|
||||
* 189..191 byte[3] reserved
|
||||
*/
|
||||
data class TaudInst(
|
||||
var index: Int,
|
||||
@@ -2645,27 +2656,41 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
var panEnvelopes: Array<TaudInstEnvPoint>, // 25 points
|
||||
var pfEnvelopes: Array<TaudInstEnvPoint>, // 25 points (pitch/filter)
|
||||
var volumeFadeoutLow: Int, // byte 172
|
||||
var fadeoutHighVibDepth: Int, // byte 173
|
||||
var fadeoutHigh: Int, // byte 173 (low nibble — 0b 0000 ffff)
|
||||
var volumeSwing: Int, // byte 174
|
||||
var vibratoSpeed: Int, // byte 175
|
||||
var vibratoSweep: Int, // byte 176
|
||||
var vibratoSweep: Int, // byte 176 (FT2 ramp ticks)
|
||||
var defaultPan: Int, // byte 177
|
||||
var pitchPanCentre: Int, // bytes 178-179
|
||||
var pitchPanSeparation: Int, // byte 180 (signed)
|
||||
var panSwing: Int, // byte 181
|
||||
var defaultCutoff: Int, // byte 182
|
||||
var defaultResonance: Int // byte 183
|
||||
var defaultResonance: Int, // byte 183
|
||||
var sampleDetune: Int, // bytes 184-185 (signed 4096-TET stored as u16)
|
||||
var instrumentFlag: Int, // byte 186 (NNA + vibrato waveform)
|
||||
var vibratoDepth: Int, // byte 187 (0..255 full range)
|
||||
var vibratoRate: Int // byte 188 (IT samplewise Vir)
|
||||
) {
|
||||
constructor(index: Int) : this(
|
||||
index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
|
||||
Array(25) { TaudInstEnvPoint(0x3F, 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
|
||||
)
|
||||
|
||||
// Reserved padding at offsets 184..191 (8 bytes per instrument).
|
||||
private val reserved = ByteArray(8)
|
||||
/** New note action — instrumentFlag bits 0-1.
|
||||
* 0=note off, 1=note cut, 2=continue, 3=note fade. */
|
||||
val newNoteAction: Int get() = instrumentFlag and 0x03
|
||||
/** Auto-vibrato waveform — instrumentFlag bits 2-4.
|
||||
* 0=sine, 1=ramp-down, 2=square, 3=random, 4=ramp-up (FT2). */
|
||||
val vibratoWaveform: Int get() = (instrumentFlag ushr 2) and 0x07
|
||||
/** Sample detune as a signed 4096-TET delta. */
|
||||
val sampleDetuneSigned: Int get() = sampleDetune.toShort().toInt()
|
||||
|
||||
// Reserved padding at offsets 189..191 (3 bytes per instrument).
|
||||
private val reserved = ByteArray(3)
|
||||
|
||||
// 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.
|
||||
@@ -2731,7 +2756,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
171 -> instGlobalVolume.toByte()
|
||||
172 -> volumeFadeoutLow.toByte()
|
||||
173 -> fadeoutHighVibDepth.toByte()
|
||||
173 -> fadeoutHigh.toByte()
|
||||
174 -> volumeSwing.toByte()
|
||||
175 -> vibratoSpeed.toByte()
|
||||
176 -> vibratoSweep.toByte()
|
||||
@@ -2742,7 +2767,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
181 -> panSwing.toByte()
|
||||
182 -> defaultCutoff.toByte()
|
||||
183 -> defaultResonance.toByte()
|
||||
in 184..191 -> reserved[offset - 184]
|
||||
184 -> sampleDetune.toByte()
|
||||
185 -> sampleDetune.ushr(8).toByte()
|
||||
186 -> instrumentFlag.toByte()
|
||||
187 -> vibratoDepth.toByte()
|
||||
188 -> vibratoRate.toByte()
|
||||
in 189..191 -> reserved[offset - 189]
|
||||
else -> throw InternalError("Bad offset $offset")
|
||||
}
|
||||
|
||||
@@ -2781,7 +2811,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
|
||||
171 -> { instGlobalVolume = byte and 0xFF }
|
||||
172 -> { volumeFadeoutLow = byte and 0xFF }
|
||||
173 -> { fadeoutHighVibDepth = byte and 0xFF }
|
||||
173 -> { fadeoutHigh = byte and 0x0F } // low nibble only (0b 0000 ffff)
|
||||
174 -> { volumeSwing = byte and 0xFF }
|
||||
175 -> { vibratoSpeed = byte and 0xFF }
|
||||
176 -> { vibratoSweep = byte and 0xFF }
|
||||
@@ -2792,7 +2822,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
181 -> { panSwing = byte and 0xFF }
|
||||
182 -> { defaultCutoff = byte and 0xFF }
|
||||
183 -> { defaultResonance = byte and 0xFF }
|
||||
in 184..191 -> { reserved[offset - 184] = byte.toByte() }
|
||||
184 -> { sampleDetune = (sampleDetune and 0xff00) or byte }
|
||||
185 -> { sampleDetune = (sampleDetune and 0x00ff) or (byte shl 8) }
|
||||
186 -> { instrumentFlag = byte and 0xFF }
|
||||
187 -> { vibratoDepth = byte and 0xFF }
|
||||
188 -> { vibratoRate = byte and 0xFF }
|
||||
in 189..191 -> { reserved[offset - 189] = byte.toByte() }
|
||||
else -> throw InternalError("Bad offset $offset")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user