mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
fix: no-param note handling divergence
This commit is contained in:
53
it2taud.py
53
it2taud.py
@@ -1232,10 +1232,20 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
vol_sus = idata.get('vol_sus', USE_ENV_BIT)
|
||||
pan_sus = idata.get('pan_sus', 0)
|
||||
pf_sus = idata.get('pf_sus', 0)
|
||||
inst_gv = idata.get('inst_gv', 0xFF)
|
||||
# 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)
|
||||
# Sample-mode default IGV: fold sample default vol (Sv) and sample GV
|
||||
# into Taud's IGV. Instrument-mode supplies inst_gv pre-folded.
|
||||
if 'inst_gv' in idata:
|
||||
inst_gv = idata['inst_gv']
|
||||
else:
|
||||
smp_vol_default = min(getattr(s, 'vol', 64), 64)
|
||||
smp_gv_default = min(getattr(s, 'gv', 64), 64)
|
||||
inst_gv = min(255, round(smp_vol_default * smp_gv_default * 255 / (64 * 64)))
|
||||
# IT fadeout (file-stored 0..2048; ITTECH practical max ≈ 1024) maps verbatim to
|
||||
# the Taud 12-bit fadeStep. The player picks divisor 1024 in IT mode (vs 65536
|
||||
# in FT2 mode) so that one fadeStep unit per tick matches Schism's
|
||||
# `chan->fadeout_volume -= (stored<<5)<<1` semantics (sndmix.c:331-339,
|
||||
# effects.c:1261). Clamp defensively to 4095.
|
||||
fadeout = min(0xFFF, idata.get('fadeout', 0) & 0xFFFF)
|
||||
|
||||
struct.pack_into('<H', inst_bin, base + 15, vol_sus & 0xFFFF)
|
||||
struct.pack_into('<H', inst_bin, base + 17, pan_sus & 0xFFFF)
|
||||
@@ -1322,7 +1332,6 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
|
||||
rows = chunk_grid[ch_idx] if ch_idx < len(chunk_grid) else [ITRow()] * PATTERN_ROWS
|
||||
last_inst = 0
|
||||
last_note_it = -1
|
||||
last_vol = None
|
||||
|
||||
for r, cell in enumerate(rows[:PATTERN_ROWS]):
|
||||
# ── Resolve vol-col into overrides ──────────────────────────────────
|
||||
@@ -1356,30 +1365,21 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
|
||||
note_triggers = (0 <= (cell.note if cell.note >= 0 else -1) <= 119)
|
||||
|
||||
# ── Volume column ────────────────────────────────────────────────────
|
||||
# Priority: explicit cell vol (from vol-col 0-64) > note-trigger default
|
||||
# > retrigger recall > vol-col slide > main-effect vol override > nop
|
||||
# Priority: explicit cell vol (vol-col 0-64) > vol-col slide > main-
|
||||
# effect vol override > nop. The per-instrument default volume is
|
||||
# baked into IGV (byte 171), so the engine resolves note-trigger
|
||||
# default volume itself; the converter no longer emits SEL_SET=Sv.
|
||||
if cell.volcol >= 0 and cell.volcol <= VC_VOL_HI:
|
||||
vol_sel, vol_value = SEL_SET, min(cell.volcol, 0x3F)
|
||||
elif note_triggers and cell.inst > 0:
|
||||
vol_sel = SEL_SET
|
||||
vol_value = inst_vols.get(last_inst, 0x3F)
|
||||
elif note_triggers and last_vol is not None:
|
||||
vol_sel, vol_value = SEL_SET, last_vol
|
||||
elif (cell.inst > 0 and cell.note < 0
|
||||
and last_note_it >= 0 and last_vol is not None):
|
||||
# Instrument-only retrigger: restate last volume
|
||||
vol_sel, vol_value = SEL_SET, last_vol
|
||||
elif vol_override is not None:
|
||||
vol_sel, vol_value = vol_override
|
||||
elif vs != SEL_FINE or vv != 0:
|
||||
vol_sel, vol_value = vs, vv
|
||||
elif vol_override is not None:
|
||||
vol_sel, vol_value = vol_override
|
||||
else:
|
||||
vol_sel, vol_value = SEL_FINE, 0
|
||||
|
||||
if cell.note is not None and 0 <= (cell.note if cell.note >= 0 else -1) <= 119:
|
||||
last_note_it = cell.note
|
||||
if vol_sel == SEL_SET:
|
||||
last_vol = vol_value
|
||||
|
||||
# ── Pan column ───────────────────────────────────────────────────────
|
||||
if cell.pan_set is not None:
|
||||
@@ -1611,15 +1611,18 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
src_smp = samples[si]
|
||||
proxy[taud_slot] = src_smp
|
||||
# IT cell-trigger initial volume comes from the sample's default
|
||||
# volume (Dv, 0..64), not the instrument's global volume.
|
||||
# volume (Sv, 0..64). It is folded into the Taud instrument's IGV
|
||||
# (byte 171) along with IT inst.gv (0..128) and sample gv (0..64),
|
||||
# so the engine applies all three as a single multiplier on every
|
||||
# fresh trigger. inst_vols is retained only for legacy callers.
|
||||
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.
|
||||
# IT inst.gv (0..128) * sample.gv (0..64) * sample.vol (0..64)
|
||||
# collapse into Taud's single instrumentwise IGV (0..255).
|
||||
smp_gv = min(getattr(src_smp, 'gv', 64), 64)
|
||||
inst_gv_255 = min(255, round(inst.gv * smp_gv * 255 / (128 * 64)))
|
||||
inst_gv_255 = min(255, round(inst.gv * smp_gv * smp_default_vol * 255
|
||||
/ (128 * 64 * 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
|
||||
|
||||
26
mod2taud.py
26
mod2taud.py
@@ -522,7 +522,10 @@ def build_sample_inst_bin(samples: list) -> tuple:
|
||||
struct.pack_into('<H', inst_bin, base + 19, 0)
|
||||
inst_bin[base + 21] = env_vol
|
||||
inst_bin[base + 22] = 0
|
||||
inst_bin[base + 171] = 0xFF # instrument global volume
|
||||
# Instrument Global Volume carries the MOD sample's default volume (0..64 → 0..255).
|
||||
# The pattern builder no longer emits SEL_SET=Sv on note triggers; the engine
|
||||
# multiplies by IGV instead, so the per-instrument level lives here.
|
||||
inst_bin[base + 171] = min(0xFF, round(min(s.volume, 64) * 255 / 64))
|
||||
inst_bin[base + 177] = 0x80 # default pan = centre (unused; pan env "p" flag not set)
|
||||
inst_bin[base + 182] = 0xFF # filter cutoff = off
|
||||
inst_bin[base + 183] = 0xFF # filter resonance = off
|
||||
@@ -546,15 +549,15 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
|
||||
inst_vols: dict) -> bytes:
|
||||
"""Build a 512-byte Taud pattern for one MOD channel.
|
||||
|
||||
Volume column rules (mirrors s3m2taud):
|
||||
explicit Cxx vol > note-trigger inst default > instrument-only retrigger
|
||||
recall > vol_override from effect > no-op.
|
||||
Volume column: explicit Cxx → SEL_SET; effect-folded vol slide → vol_override;
|
||||
otherwise SEL_FINE/0 (no-op). Per-instrument default volume lives in IGV
|
||||
(byte 171) and is applied by the engine on every fresh trigger — the
|
||||
converter no longer has to emit SEL_SET=Sv to scale notes.
|
||||
"""
|
||||
out = bytearray(PATTERN_BYTES)
|
||||
rows = grid[ch_idx] if ch_idx < len(grid) else [ModRow()] * MOD_PATTERN_ROWS
|
||||
last_inst = 0
|
||||
last_period = 0
|
||||
last_vol = None
|
||||
for r, row in enumerate(rows[:MOD_PATTERN_ROWS]):
|
||||
note_taud = period_to_taud_note(row.period)
|
||||
note_triggers = (row.period > 0)
|
||||
@@ -562,10 +565,6 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
|
||||
if row.inst > 0:
|
||||
last_inst = row.inst
|
||||
|
||||
retrigger = (row.inst > 0
|
||||
and row.period == 0
|
||||
and last_period > 0)
|
||||
|
||||
op, arg, vol_override, pan_override = encode_effect(
|
||||
row.effect, row.effect_arg, ch_idx, r)
|
||||
|
||||
@@ -575,13 +574,6 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
|
||||
if vol_override is not None and vol_override[0] != SEL_SET:
|
||||
vprint(f" ch{ch_idx} row{r}: dropped vol slide "
|
||||
f"(cell already carries explicit Cxx volume)")
|
||||
elif note_triggers and row.inst > 0:
|
||||
vol_sel = SEL_SET
|
||||
vol_value = inst_vols.get(last_inst, 0x3F)
|
||||
elif note_triggers and last_vol is not None:
|
||||
vol_sel, vol_value = SEL_SET, last_vol
|
||||
elif retrigger and last_vol is not None:
|
||||
vol_sel, vol_value = SEL_SET, last_vol
|
||||
elif vol_override is not None:
|
||||
vol_sel, vol_value = vol_override
|
||||
else:
|
||||
@@ -589,8 +581,6 @@ def build_pattern(grid: list, ch_idx: int, default_pan: int,
|
||||
|
||||
if note_triggers:
|
||||
last_period = row.period
|
||||
if vol_sel == SEL_SET:
|
||||
last_vol = vol_value
|
||||
|
||||
# ── Pan column ──
|
||||
if pan_override is not None:
|
||||
|
||||
46
s3m2taud.py
46
s3m2taud.py
@@ -514,7 +514,10 @@ def build_sample_inst_bin(instruments: list) -> tuple:
|
||||
# Volume env point 0: hold at env_vol indefinitely (offset minifloat = 0 → hold).
|
||||
inst_bin[base + 21] = env_vol
|
||||
inst_bin[base + 22] = 0
|
||||
inst_bin[base + 171] = 0xFF # instrument global volume
|
||||
# Instrument Global Volume carries the S3M instrument's default volume (0..64 → 0..255).
|
||||
# The pattern builder no longer emits SEL_SET=Sv on note triggers; the engine
|
||||
# multiplies by IGV instead, so the per-instrument level lives here.
|
||||
inst_bin[base + 171] = min(0xFF, round(min(inst.volume, 64) * 255 / 64))
|
||||
inst_bin[base + 177] = 0x80 # default pan = centre (unused; pan env "p" flag not set)
|
||||
inst_bin[base + 182] = 0xFF # filter cutoff = off
|
||||
inst_bin[base + 183] = 0xFF # filter resonance = off
|
||||
@@ -544,11 +547,10 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
|
||||
amiga_mode: bool = False) -> bytes:
|
||||
"""Build a 512-byte Taud pattern for one S3M channel.
|
||||
|
||||
Volume column: explicit S3M cell vol → SEL_SET; when a note triggers
|
||||
with no explicit vol, emit SEL_SET using the instrument's default volume
|
||||
(looked up from inst_vols, a 1-based inst index → 0..63 volume dict).
|
||||
M/N/K/L overrides apply only when the cell has no explicit vol and no
|
||||
note trigger. Otherwise SEL_FINE/0 (no-op).
|
||||
Volume column: explicit S3M cell vol -> SEL_SET; M/N/K/L vol slides folded
|
||||
by encode_effect -> vol_override; otherwise SEL_FINE/0 (no-op). Per-
|
||||
instrument default volume lives in IGV (byte 171) and is applied by the
|
||||
engine on every fresh trigger, so the converter no longer emits SEL_SET=Sv.
|
||||
Pan column: row 0 emits SEL_SET = default_pan to position the channel;
|
||||
other rows default to SEL_FINE/0 unless an X/P/etc effect overrides.
|
||||
"""
|
||||
@@ -557,55 +559,27 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
|
||||
out = bytearray(PATTERN_BYTES)
|
||||
rows = s3m_grid[ch_idx] if ch_idx < len(s3m_grid) else [S3MRow()] * PATTERN_ROWS
|
||||
last_inst = 0 # 1-based; tracks which instrument is loaded on this channel
|
||||
last_note = S3M_NOTE_EMPTY # last raw S3M note byte that was a real pitch
|
||||
last_vol = None # last SEL_SET volume value (0-63), for retrigger recall
|
||||
for r, row in enumerate(rows[:PATTERN_ROWS]):
|
||||
note = encode_note(row.note)
|
||||
inst = row.inst # S3M 1-based → Taud 1-based
|
||||
inst = row.inst # S3M 1-based -> Taud 1-based
|
||||
|
||||
if row.inst > 0:
|
||||
last_inst = row.inst
|
||||
|
||||
# ── Instrument-only retrigger ──
|
||||
# Instrument-only row: recall the last volume without touching the note.
|
||||
retrigger = (row.inst > 0
|
||||
and row.note == S3M_NOTE_EMPTY
|
||||
and last_note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF))
|
||||
|
||||
op, arg, vol_override, pan_override = encode_effect(
|
||||
row.effect, row.effect_arg, ch_idx, r, amiga_mode=amiga_mode)
|
||||
|
||||
# ── Volume column ──
|
||||
note_triggers = (row.note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF))
|
||||
# -- Volume column --
|
||||
if row.vol >= 0:
|
||||
vol_sel, vol_value = SEL_SET, min(row.vol, 0x3F)
|
||||
if vol_override is not None and vol_override[0] != SEL_SET:
|
||||
vprint(f" ch{ch_idx} row{r}: dropped vol slide "
|
||||
f"(cell already carries explicit volume)")
|
||||
elif note_triggers and row.inst > 0:
|
||||
# Note trigger with a fresh instrument: use that instrument's
|
||||
# default volume.
|
||||
vol_sel = SEL_SET
|
||||
vol_value = inst_vols.get(last_inst, 0x3F)
|
||||
elif note_triggers and last_vol is not None:
|
||||
# Note trigger without instrument: keep the channel's current
|
||||
# volume rather than resetting to the instrument default.
|
||||
vol_sel, vol_value = SEL_SET, last_vol
|
||||
elif retrigger and last_vol is not None:
|
||||
# Instrument-only row: re-emit the last known volume so the sample
|
||||
# restarts at the correct level without an explicit note trigger.
|
||||
vol_sel, vol_value = SEL_SET, last_vol
|
||||
elif vol_override is not None:
|
||||
vol_sel, vol_value = vol_override
|
||||
else:
|
||||
vol_sel, vol_value = SEL_FINE, 0 # no-op fine slide
|
||||
|
||||
# Track note and volume for future retrigger lookups.
|
||||
if row.note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF):
|
||||
last_note = row.note
|
||||
if vol_sel == SEL_SET:
|
||||
last_vol = vol_value
|
||||
|
||||
# ── Pan column ──
|
||||
if pan_override is not None:
|
||||
pan_sel, pan_value = pan_override
|
||||
|
||||
@@ -2109,9 +2109,10 @@ TODO:
|
||||
[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] 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
|
||||
[x] 4THSYM.it: pitchbend is wrong, some notes keep playing (loudly!) even if new notes are emitted
|
||||
[x] `*2taud.py`: some notes are emitted with wrong volume-set command. Tested with GSLINGER.mod: on order 0x15 channel 1, mod2taud.py emits volume 8 -- also many of the effects are dropped. Suggested solution: currently all converters write default volume to the voleff when original modules (.mod/.s3m/.it) specify nothing; we should also write nothing and let the engine resolve the value just like other trackers do (also we now have "Instrument Global Volume" on instrument definition unlike the other time). This bug may affecting other formats, not just mod2taud.py, as well
|
||||
[ ] nearly_there_.mod: `C#5 SD300 / ... / C-5 SD200 / A#4 / G#4 (at tickspeed 4)`: every `C-5 SD200` (there are four occurances) gets skipped
|
||||
[ ] scale Oxxxx when samples get resampled
|
||||
[ ] implement bitcrusher and overdrive (eff sym '8' and '9')
|
||||
|
||||
|
||||
|
||||
@@ -1553,9 +1553,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
voice.basePitch = noteVal
|
||||
voice.amigaPeriod = -1.0 // fresh trigger: period state must reseed from the new noteVal
|
||||
voice.playbackRate = computePlaybackRate(inst, noteVal)
|
||||
if (volOverride >= 0) {
|
||||
voice.channelVolume = volOverride.coerceIn(0, 0x3F)
|
||||
}
|
||||
// Fresh trigger resets channel volume to full ($3F). Per-instrument scaling lives in
|
||||
// instGlobalVolume (byte 171), which the mixer applies as a multiplier. Converters
|
||||
// therefore no longer need to emit SEL_SET=Sv on note-trigger rows.
|
||||
voice.channelVolume = if (volOverride >= 0) volOverride.coerceIn(0, 0x3F) else 0x3F
|
||||
voice.rowVolume = voice.channelVolume
|
||||
voice.noteWasCut = false
|
||||
voice.noteFading = false
|
||||
@@ -1793,7 +1794,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// ── Note ──
|
||||
val toneG = (row.effect == EffectOp.OP_G)
|
||||
when (row.note) {
|
||||
0xFFFF -> {} // no-op
|
||||
// No note but an instrument byte is present: latch the instrument so
|
||||
// the *next* note-only trigger picks up the right sample. Trackers
|
||||
// call this an "instrument-only retrigger"; in MOD/S3M/IT the sample
|
||||
// keeps playing, but the channel's instrument reference advances.
|
||||
0xFFFF -> { if (row.instrment != 0) voice.instrumentId = row.instrment }
|
||||
0x0000 -> { voice.keyOff = true; voice.active = false } // key-off; breaks sustain loop
|
||||
0xFFFE -> voice.active = false // note cut
|
||||
else -> {
|
||||
@@ -1806,7 +1811,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
voice.noteDelayTick = (row.effectArg ushr 8) and 0xF
|
||||
voice.delayedNote = row.note
|
||||
voice.delayedInst = row.instrment
|
||||
voice.delayedVol = if (row.volume >= 0) row.volume else -1
|
||||
// Only treat the vol cell as an override when it carries SEL_SET;
|
||||
// SEL_FINE/0 (no-op) and slide selectors must not collapse into
|
||||
// a SET=0 on the deferred trigger.
|
||||
voice.delayedVol = if (row.volumeEff == 0) row.volume else -1
|
||||
} else {
|
||||
applyDuplicateCheck(ts, vi, row.instrment, row.note)
|
||||
maybeSpawnBackgroundForNNA(ts, voice, vi)
|
||||
@@ -2211,10 +2219,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
// Auto-vibrato (instrument-supplied sample LFO) — added on top of pitchToMixer.
|
||||
val autoVibDelta = advanceAutoVibrato(voice, inst)
|
||||
|
||||
// Pitch envelope contribution: env value 0..1, 0.5 = unity. -32..+32
|
||||
// semitone range maps to ±32 × 4096/12 ≈ ±10923 4096-TET units.
|
||||
// Pitch envelope contribution: env value 0..1, 0.5 = unity.
|
||||
// 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.hasPfEnv && voice.pfEnvOn && !voice.envPfIsFilter)
|
||||
((voice.envPfValue - 0.5) * 2.0 * 32.0 * 4096.0 / 12.0).toInt()
|
||||
((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
||||
else 0
|
||||
|
||||
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE)
|
||||
@@ -2236,13 +2246,18 @@ 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 / 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).
|
||||
// Volume fadeout: after key-off OR Note-Fade NNA, decrement per tick.
|
||||
// The 12-bit fadeStep is split across volumeFadeoutLow + low nibble of fadeoutHigh.
|
||||
// Divisor selects per-tracker semantics:
|
||||
// FT2 mode (fadeoutCutOnZero=true): fadeStep / 65536 per tick — matches FT2 .XM (16-bit accumulator, decrement = stored).
|
||||
// IT mode (fadeoutCutOnZero=false): fadeStep / 1024 per tick — matches Schism (sndmix.c:331-339 + effects.c:1261:
|
||||
// accumulator 65536, decrement = (stored<<5)<<1 = stored·64).
|
||||
// Stored 0: FT2 mode cuts on key-off; IT mode leaves voice playing (no fade).
|
||||
if (voice.keyOff || voice.noteFading) {
|
||||
val fadeStep = inst.volumeFadeoutLow or ((inst.fadeoutHigh and 0x0F) shl 8)
|
||||
if (fadeStep > 0) {
|
||||
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / 65536.0).coerceAtLeast(0.0)
|
||||
val divisor = if (ts.fadeoutCutOnZero) 65536.0 else 1024.0
|
||||
voice.fadeoutVolume = (voice.fadeoutVolume - fadeStep / divisor).coerceAtLeast(0.0)
|
||||
if (voice.fadeoutVolume <= 0.0) voice.active = false
|
||||
} else if (ts.fadeoutCutOnZero) {
|
||||
voice.active = false
|
||||
@@ -2297,7 +2312,8 @@ 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 / 65536.0).coerceAtLeast(0.0)
|
||||
val divisor = if (ts.fadeoutCutOnZero) 65536.0 else 1024.0
|
||||
bg.fadeoutVolume = (bg.fadeoutVolume - fadeStep / divisor).coerceAtLeast(0.0)
|
||||
} else if (ts.fadeoutCutOnZero) {
|
||||
bg.active = false
|
||||
bgIt.remove()
|
||||
@@ -2307,7 +2323,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.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
|
||||
((bg.envPfValue - 0.5) * 2.0 * 32.0 * 4096.0 / 12.0).toInt()
|
||||
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
||||
else 0
|
||||
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0, 0xFFFE)
|
||||
bg.playbackRate = computePlaybackRate(inst, finalPitch)
|
||||
|
||||
Reference in New Issue
Block a user