mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
instrument volume fadeout
This commit is contained in:
@@ -864,9 +864,9 @@ Effects in this section modifies the behaviour of the mixer. Primary intention o
|
||||
|
||||
## 1 $xx00 — Global behaviour flags
|
||||
|
||||
**Plain.** Sets how the mixer should treat the panning. Available flags are:
|
||||
**Plain.** Sets mixer-wide behaviour flags. Available flags are:
|
||||
|
||||
0b 0000 00fp
|
||||
0b 0000 0mfp
|
||||
|
||||
- p unset: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting.
|
||||
- p set: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned.
|
||||
@@ -874,6 +874,9 @@ Effects in this section modifies the behaviour of the mixer. Primary intention o
|
||||
- f unset: Linear tone mode. Pitch shift will behave like MIDI/ImpulseTracker/ScreamTracker linear mode. **Coarse and fine E/F arguments are stored as 4096-TET pitch units** and subtracted/added directly from the stored pitch.
|
||||
- f set: Amiga (cycle-based) tone mode. Pitch shift will behave like ProTracker/ScreamTracker default mode. **Coarse and fine E/F arguments are stored as raw tracker period units** (the unscaled byte/nibble from the source PT/S3M/IT file) and applied in Amiga period space. Tone portamento (G) remains linear regardless of mode.
|
||||
|
||||
- m unset: IT fadeout-zero policy. An instrument with stored volume fadeout = 0 does **not** fade out on key-off; the voice plays through until the volume envelope ends it (or never, if there is no envelope).
|
||||
- m set: FT2 fadeout-zero policy. An instrument with stored volume fadeout = 0 is **cut** on the first tick after key-off (or NNA Note-Fade). Nonzero fadeouts behave identically in both modes — the per-tick decrement is always `fadeout / 65536` in unity-volume units.
|
||||
|
||||
**Implementation.**
|
||||
- Panning-linear:
|
||||
- L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
|
||||
|
||||
@@ -472,7 +472,7 @@ class ITInstrument:
|
||||
# *_env_sustain: int (16-bit, 0b 0ut sssss pcb eeeee), 0 = no envelope
|
||||
# pf_is_filter: bool — pf envelope mode (False = pitch, True = filter)
|
||||
# ifc / ifr : initial filter cutoff / resonance (0..127, 0 if not set)
|
||||
# fadeout : 0..1024 (IT FadeOut field, applied per tick after key-off)
|
||||
# fadeout : 0..1024 (IT FadeOut field; doubled to 0..2048 when written to Taud's 12-bit field)
|
||||
# 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)
|
||||
@@ -1222,7 +1222,9 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
pan_sus = idata.get('pan_sus', 0)
|
||||
pf_sus = idata.get('pf_sus', 0)
|
||||
inst_gv = idata.get('inst_gv', 0xFF)
|
||||
fadeout = idata.get('fadeout', 0) & 0x3FF # 10-bit (low 8 + high 2)
|
||||
# IT fadeout (0..1024) is in half-units of Taud's per-tick scale; double to align with
|
||||
# FT2 / native Taud (12-bit, engine subtracts fadeout/65536 per tick). Clamp defensively.
|
||||
fadeout = min(0xFFF, (idata.get('fadeout', 0) & 0xFFFF) * 2)
|
||||
|
||||
struct.pack_into('<H', inst_bin, base + 15, vol_sus & 0xFFFF)
|
||||
struct.pack_into('<H', inst_bin, base + 17, pan_sus & 0xFFFF)
|
||||
@@ -1741,6 +1743,7 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
vprint(f" cue sheet: {len(sheet)} → {len(cue_comp)} bytes (gzip)")
|
||||
|
||||
# flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted)
|
||||
# bit 2 (m) cleared: IT fadeout-zero policy — stored fadeout=0 means "no fadeout".
|
||||
flags_byte = 0x00 if h.linear_slides else 0x02
|
||||
# IT global/mix volumes are 0..128; rescale to Taud's 0..255 (clamped).
|
||||
global_vol_taud = min(0xFF, round(h.global_vol * 255 / 128))
|
||||
|
||||
@@ -758,7 +758,9 @@ def assemble_taud(mod: dict) -> bytes:
|
||||
# ProTracker is Amiga-period-based by definition, so we set the f bit so
|
||||
# the engine applies coarse pitch slides in period space (recovers PT's
|
||||
# characteristic non-linear pitch character).
|
||||
flags_byte = 0x02
|
||||
# bit 2 (m) set: FT2 fadeout-zero policy — PT has no fadeout, so the stored
|
||||
# zero on every instrument means "cut on key-off" (unified with S3M imports).
|
||||
flags_byte = 0x02 | 0x04
|
||||
song_table = encode_song_entry(
|
||||
song_offset=song_offset,
|
||||
num_voices=n_channels,
|
||||
|
||||
@@ -831,7 +831,9 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
|
||||
|
||||
# Song table row (32 bytes; see encode_song_entry).
|
||||
# flags byte: bit 1 (f) = Amiga pitch-slide mode (mirrors the S3M linear_slides flag inverted)
|
||||
flags_byte = 0x00 if h.linear_slides else 0x02
|
||||
# bit 2 (m) set: FT2 fadeout-zero policy — S3M has no per-instrument fadeout field, so a
|
||||
# stored zero means "cut on key-off" (matching ST3's lineage from the FT2 family).
|
||||
flags_byte = (0x00 if h.linear_slides else 0x02) | 0x04
|
||||
song_table = encode_song_entry(
|
||||
song_offset=song_offset,
|
||||
num_voices=C,
|
||||
|
||||
@@ -2008,13 +2008,12 @@ Instrument bin: Registry for 256 instruments, formatted as:
|
||||
- IT: look for sample's SusLoop flag
|
||||
Bit16 Volume envelope sustain/loops and flags
|
||||
* Sustain is implemented by enabling 't' flag. FastTracker has no 'Sus Loop' but only 'Sus Point'; use same value for start and end index
|
||||
0b 0ut sssss pcb eeeee
|
||||
0b 0ut sssss 0cb eeeee
|
||||
s: sustain/loop start index
|
||||
e: sustain/loop end index
|
||||
|
||||
b: use envelope
|
||||
c: envelope carry
|
||||
p: (IT) fadeout is zero; (XM) fadeout is cut
|
||||
|
||||
t: the loop must sustain (key-off escapes the loop)
|
||||
u: set to enable the sustain/loop
|
||||
@@ -2055,10 +2054,18 @@ Instrument bin: Registry for 256 instruments, formatted as:
|
||||
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
|
||||
- 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)
|
||||
Uint8 Volume Fadeout low bits
|
||||
Bit8 Fadeout and vibrato
|
||||
0b 0000 ffff
|
||||
f: Volume Fadeout high bits
|
||||
* Combined 12-bit fadeout value is the engine's per-tick decrement, in 1/65536 units
|
||||
(a unity-volume voice silenced over (65536 / fadeout) ticks after key-off).
|
||||
* Stored 0: behaviour depends on Global Behaviour bit 'm' (see Song Table) —
|
||||
IT mode (m=0) leaves the voice unfaded; FT2 mode (m=1) cuts on key-off.
|
||||
* Source-format mapping:
|
||||
- IT: stored fadeout (0..1024) MUST be doubled on import (taud = it × 2);
|
||||
Taud's per-tick scale matches FT2 natively, so IT values are scaled to match.
|
||||
- FT2: stored fadeout (0..0xFFF) is passed through unchanged.
|
||||
Uint8 Volume swing (0..255 full range)
|
||||
Uint8 Vibrato speed
|
||||
* ImpulseTracker has samplewise vibrato speed (0..64), and they must be taken into account because Taud has no samplewise config
|
||||
@@ -2073,7 +2080,7 @@ Instrument bin: Registry for 256 instruments, formatted as:
|
||||
Uint8 Pan swing (0..255 full range)
|
||||
Uint8 Default cutoff (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud)
|
||||
Uint8 Default resonance (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud)
|
||||
Uint16 Sample detune (in 4096-TET unit) (XM finetune scale need to be rescaled accordingly)
|
||||
Uint16 Sample detune (in 4096-TET unit) (FT2 finetune scale need to be rescaled accordingly)
|
||||
Bit8 Instrument Flag
|
||||
0b 000 www nn
|
||||
n: New note action. 00: note off, 01: note cut, 10: continue, 11: note fade (arranged differently to IT)
|
||||
@@ -2098,7 +2105,7 @@ TODO:
|
||||
[x] implement sample loop sustain
|
||||
"Caveat: on a foreground voice, key-off (row.note == 0x0000) currently sets voice.active = false at AudioAdapter.kt:1713, which silences the channel immediately. Sustain-loop escape therefore only takes effect on background voices spawned by NNA "Note Off" — which matches the IT idiom of layering a new note over a sustained one. Let me know if you also want the foreground key-off to keep the voice playing through fadeout."
|
||||
[x] 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] 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
|
||||
[ ] implement bitcrusher (eff sym '8')
|
||||
|
||||
|
||||
@@ -2314,6 +2321,11 @@ Endianness: Little
|
||||
Uint16 Current Tuning base note (1..65533). A4 (western default) is 0x5C00. C9 (tracker default) is 0xA000. If zero, assume the tracker default value
|
||||
Float32 Frequency at the base note. Tracker default is 8363.0. If zero, assume the tracker default
|
||||
Uint8 Flags for Global Behaviour (effect symbol '1')
|
||||
0b 0000 0mfp
|
||||
p: panning law (0=linear, 1=equal-power)
|
||||
f: tone mode (0=linear pitch slides, 1=Amiga period slides)
|
||||
m: fadeout-zero policy (0=IT — stored fadeout 0 means no fadeout;
|
||||
1=FT2 — stored fadeout 0 means cut on key-off)
|
||||
Uint8 Song global volume
|
||||
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
|
||||
Uint8 Song mixing volume
|
||||
|
||||
@@ -1754,9 +1754,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// 1 $xx00 — Global behaviour flags byte in the high byte (see TAUD_NOTE_EFFECTS.md §1).
|
||||
// bit 0 (p): 0=linear pan, 1=equal-power pan
|
||||
// bit 1 (f): 0=linear pitch slides, 1=Amiga-mode pitch slides
|
||||
// bit 2 (m): fadeout-zero policy. 0=IT (stored 0 ⇒ no fadeout), 1=FT2 (stored 0 ⇒ cut on key-off)
|
||||
val flags = rawArg ushr 8
|
||||
ts.panLaw = flags and 1
|
||||
ts.amigaMode = (flags and 2) != 0
|
||||
ts.fadeoutCutOnZero = (flags and 4) != 0
|
||||
}
|
||||
EffectOp.OP_A -> {
|
||||
val tr = (rawArg ushr 8) and 0xFF
|
||||
@@ -2141,12 +2143,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// Refresh biquad filter coefficients once per tick (only recomputes when changed).
|
||||
refreshVoiceFilter(voice)
|
||||
|
||||
// Volume fadeout: after key-off OR Note-Fade NNA, decrement by inst.volumeFadeout / 1024 per tick.
|
||||
// The 10-bit fadeout value is split across volumeFadeoutLow + low nibble of fadeoutHigh.
|
||||
// Volume fadeout: after key-off OR Note-Fade NNA, decrement by inst.volumeFadeout / 65536 per tick.
|
||||
// The 12-bit fadeout value is split across volumeFadeoutLow + low nibble of fadeoutHigh.
|
||||
// Stored 0: with fadeoutCutOnZero (FT2 mode) the voice is cut on key-off; otherwise no fadeout (IT mode).
|
||||
if (voice.keyOff || voice.noteFading) {
|
||||
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8)
|
||||
if (fadeStep > 0) {
|
||||
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / 1024.0).coerceAtLeast(0.0)
|
||||
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / 65536.0).coerceAtLeast(0.0)
|
||||
if (voice.fadeoutVolume <= 0.0) voice.active = false
|
||||
} else if (ts.fadeoutCutOnZero) {
|
||||
voice.active = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2198,7 +2204,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
if (bg.keyOff || bg.noteFading) {
|
||||
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8)
|
||||
if (fadeStep > 0) {
|
||||
bg.fadeoutVolume = (bg.fadeoutVolume - fadeStep / 1024.0).coerceAtLeast(0.0)
|
||||
bg.fadeoutVolume = (bg.fadeoutVolume - fadeStep / 65536.0).coerceAtLeast(0.0)
|
||||
} else if (ts.fadeoutCutOnZero) {
|
||||
bg.active = false
|
||||
bgIt.remove()
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Auto-vibrato keeps running on backgrounds — it's an instrument-intrinsic LFO.
|
||||
@@ -2662,6 +2672,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// Global mixer config (effect 1).
|
||||
var panLaw = 0 // 0 = linear balance (default), 1 = equal-power
|
||||
var amigaMode = false // false = linear pitch slides, true = Amiga period-space slides
|
||||
var fadeoutCutOnZero = false // false = IT (stored 0 ⇒ no fadeout); true = FT2 (stored 0 ⇒ cut on key-off)
|
||||
|
||||
// Pending row-end events (set during a row by B/C; consumed at row end).
|
||||
var pendingOrderJump = -1 // -1 = none; otherwise the order index to jump to
|
||||
@@ -2772,7 +2783,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
} }
|
||||
7 -> if (isPcmMode) { pcmUpload = true } else {
|
||||
initialGlobalFlags = byte
|
||||
trackerState?.let { ts -> ts.panLaw = byte and 1; ts.amigaMode = (byte and 2) != 0 }
|
||||
trackerState?.let { ts ->
|
||||
ts.panLaw = byte and 1
|
||||
ts.amigaMode = (byte and 2) != 0
|
||||
ts.fadeoutCutOnZero = (byte and 4) != 0
|
||||
}
|
||||
}
|
||||
8 -> { bpm = byte + 24 }
|
||||
9 -> { tickRate = byte }
|
||||
@@ -2807,6 +2822,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
ts.finePatternDelayExtra = 0
|
||||
ts.panLaw = initialGlobalFlags and 1
|
||||
ts.amigaMode = (initialGlobalFlags and 2) != 0
|
||||
ts.fadeoutCutOnZero = (initialGlobalFlags and 4) != 0
|
||||
ts.voices.forEach {
|
||||
it.active = false
|
||||
it.channelVolume = 0x3F
|
||||
|
||||
Reference in New Issue
Block a user