mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
volume policy when unspecified: retrigger (note+inst cmd) -> default value, no retrigger (note cmd only) -> prev value
This commit is contained in:
@@ -2339,13 +2339,15 @@ TODO:
|
|||||||
Hz values verbatim (no SLIDE_UNITS_PER_HZ scaling) and sets the
|
Hz values verbatim (no SLIDE_UNITS_PER_HZ scaling) and sets the
|
||||||
linear-freq flag in the song-table flags byte. Spec details in
|
linear-freq flag in the song-table flags byte. Spec details in
|
||||||
TAUD_NOTE_EFFECTS.md §1, §E, §F, §G.
|
TAUD_NOTE_EFFECTS.md §1, §E, §F, §G.
|
||||||
[ ] milkytracker-style volume ramping (on sample-end only)
|
[x] milkytracker-style volume ramping (on sample-end only)
|
||||||
[x] make Cues tab move faster
|
[x] make Cues tab move faster
|
||||||
Resolution: Cues panel now uses memory-shift (`shiftOrdersAreaHorizontal`)
|
Resolution: Cues panel now uses memory-shift (`shiftOrdersAreaHorizontal`)
|
||||||
for LEFT/RIGHT and `shiftPatternArea` for UP/DOWN, plus per-row
|
for LEFT/RIGHT and `shiftPatternArea` for UP/DOWN, plus per-row
|
||||||
(`drawOrdersRowAt`) and per-column (`drawOrdersVoiceColumnAt`) helpers,
|
(`drawOrdersRowAt`) and per-column (`drawOrdersVoiceColumnAt`) helpers,
|
||||||
replacing the full-panel redraw on every keystroke.
|
replacing the full-panel redraw on every keystroke.
|
||||||
[x] volume and panning policy to match note effect policy: when note is "retriggerred" (note command with instrument specified), the volume/pan must take default value; if not (note command with instrument 0) the volume/pan must stay at the old value. Make both audio engine and taut.js simulator changes.
|
[x] volume and panning policy to match note effect policy: when note is "retriggerred" (note command with instrument specified), the volume/pan must take default value; if not (note command with instrument 0) the volume/pan must stay at the old value. Make both audio engine and taut.js simulator changes.
|
||||||
|
[ ] xm volume column commands (+x, -x, Dx, Lx, Mx, Px, Rx, Sx, Ux, Vx) are completely ignored
|
||||||
|
[ ] theday.xm order 0x28, channel 6..8 has 'note trigger with inst 1 but no volume -> key-off -> set-volume to 0x20 -> key-off -> set-volume to 0x10 -> key-off -> ...' and it sounds like gating: key-off silences the output, set-volume turns on the output again; notably, this behaviour only works when volume envelope is turned off (any fadeouts progress normally). What I want to know before implementing this feature is that would the way it works on XM conflicts with Taud or ImpulseTracker's behaviour
|
||||||
|
|
||||||
|
|
||||||
Play Data: play data are series of tracker-like instructions, visualised as:
|
Play Data: play data are series of tracker-like instructions, visualised as:
|
||||||
|
|||||||
@@ -139,6 +139,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// linear-freq slides — uses A0 = 27.5 Hz with the same equal-temperament tuning,
|
// linear-freq slides — uses A0 = 27.5 Hz with the same equal-temperament tuning,
|
||||||
// so emitted Hz values map directly to audible Hz at any pitch.
|
// so emitted Hz values map directly to audible Hz at any pitch.
|
||||||
const val LINEAR_FREQ_C4_HZ = 261.6255653005986
|
const val LINEAR_FREQ_C4_HZ = 261.6255653005986
|
||||||
|
// Anti-click ramp-out: when a sample naturally ends or is cut, the voice keeps
|
||||||
|
// mixing for this many output samples while gain decays linearly to 0.
|
||||||
|
// 8 ms at 32 kHz — long enough to bury the click, short enough not to read as fade.
|
||||||
|
// Applied on sample end only (preserves attack transients on note start).
|
||||||
|
const val RAMP_OUT_SAMPLES = 256
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory map (terranmon.txt:1985-1997, updated 2026-05-06):
|
// Memory map (terranmon.txt:1985-1997, updated 2026-05-06):
|
||||||
@@ -1629,6 +1634,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val s1 = (b1 - 127.5) / 127.5
|
val s1 = (b1 - 127.5) / 127.5
|
||||||
val sample = s0 + (s1 - s0) * frac
|
val sample = s0 + (s1 - s0) * frac
|
||||||
|
|
||||||
|
// While ramping out at sample end, hold position so the mixer keeps emitting the
|
||||||
|
// clamped last-sample value with decaying gain — no further advance, no re-trigger
|
||||||
|
// of the end check.
|
||||||
|
if (voice.rampOutSamples > 0) return sample
|
||||||
|
|
||||||
if (voice.forward) {
|
if (voice.forward) {
|
||||||
voice.samplePos += voice.playbackRate
|
voice.samplePos += voice.playbackRate
|
||||||
// When the sustain bit is set, key-off escapes the loop: the sample plays past
|
// When the sustain bit is set, key-off escapes the loop: the sample plays past
|
||||||
@@ -1636,10 +1646,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val effectiveLoopMode =
|
val effectiveLoopMode =
|
||||||
if (inst.sampleLoopSustain && voice.keyOff) 0 else (inst.loopMode and 3)
|
if (inst.sampleLoopSustain && voice.keyOff) 0 else (inst.loopMode and 3)
|
||||||
when (effectiveLoopMode) {
|
when (effectiveLoopMode) {
|
||||||
0 -> if (voice.samplePos >= sampleLen) voice.active = false
|
0 -> if (voice.samplePos >= sampleLen) {
|
||||||
|
voice.samplePos = (sampleLen - 1).toDouble().coerceAtLeast(0.0)
|
||||||
|
startRampOut(voice)
|
||||||
|
}
|
||||||
1 -> if (voice.samplePos >= loopEnd) voice.samplePos -= (loopEnd - loopStart).coerceAtLeast(1.0)
|
1 -> if (voice.samplePos >= loopEnd) voice.samplePos -= (loopEnd - loopStart).coerceAtLeast(1.0)
|
||||||
2 -> if (voice.samplePos >= loopEnd) { voice.samplePos = loopEnd; voice.forward = false }
|
2 -> if (voice.samplePos >= loopEnd) { voice.samplePos = loopEnd; voice.forward = false }
|
||||||
3 -> if (voice.samplePos >= sampleLen) { voice.samplePos = sampleLen.toDouble() - 1; voice.active = false }
|
3 -> if (voice.samplePos >= sampleLen) {
|
||||||
|
voice.samplePos = (sampleLen - 1).toDouble().coerceAtLeast(0.0)
|
||||||
|
startRampOut(voice)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
voice.samplePos -= voice.playbackRate
|
voice.samplePos -= voice.playbackRate
|
||||||
@@ -1648,6 +1664,19 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
return sample
|
return sample
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Engage the MilkyTracker-style sample-end ramp. The voice keeps emitting its held
|
||||||
|
* last-sample value for [RAMP_OUT_SAMPLES] more output samples while gain decays
|
||||||
|
* linearly from 1.0 to 0.0; the mixer flips voice.active = false at the end.
|
||||||
|
* No-op if already ramping (don't restart a running ramp from a re-entrant call).
|
||||||
|
*/
|
||||||
|
private fun startRampOut(voice: Voice) {
|
||||||
|
if (voice.rampOutSamples > 0) return
|
||||||
|
voice.rampOutSamples = RAMP_OUT_SAMPLES
|
||||||
|
voice.rampOutGain = 1.0
|
||||||
|
voice.rampOutStep = 1.0 / RAMP_OUT_SAMPLES
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a fresh note on [voice]: load the instrument, reset sample position, kick off the envelope.
|
* Trigger a fresh note on [voice]: load the instrument, reset sample position, kick off the envelope.
|
||||||
* Pulled out so S$Dx (note delay) can defer the same logic to a later tick.
|
* Pulled out so S$Dx (note delay) can defer the same logic to a later tick.
|
||||||
@@ -1681,6 +1710,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
voice.envPfValue = if (voice.hasPfEnv) inst.pfEnvelopes[0].value / 255.0 else 0.5
|
voice.envPfValue = if (voice.hasPfEnv) inst.pfEnvelopes[0].value / 255.0 else 0.5
|
||||||
// Fadeout starts at unity; advances only after key-off.
|
// Fadeout starts at unity; advances only after key-off.
|
||||||
voice.fadeoutVolume = 1.0
|
voice.fadeoutVolume = 1.0
|
||||||
|
// Cancel any sample-end ramp left over from the previous note — a fresh trigger's
|
||||||
|
// attack must not be muted by a trailing fade.
|
||||||
|
voice.rampOutSamples = 0
|
||||||
|
voice.rampOutGain = 0.0
|
||||||
// Auto-vibrato sweep ramp restarts on every fresh trigger.
|
// Auto-vibrato sweep ramp restarts on every fresh trigger.
|
||||||
voice.autoVibPhase = 0
|
voice.autoVibPhase = 0
|
||||||
voice.autoVibTicksSinceTrigger = 0
|
voice.autoVibTicksSinceTrigger = 0
|
||||||
@@ -2693,8 +2726,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
rGain = if (pan < 0x80) pan / 128.0 else 1.0
|
rGain = if (pan < 0x80) pan / 128.0 else 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mixL += s * vol * lGain
|
// Sample-end ramp-out: snapshot gain, advance the ramp, deactivate at zero.
|
||||||
mixR += s * vol * rGain
|
val rampGain = if (voice.rampOutSamples > 0) {
|
||||||
|
val g = voice.rampOutGain
|
||||||
|
voice.rampOutGain -= voice.rampOutStep
|
||||||
|
voice.rampOutSamples--
|
||||||
|
if (voice.rampOutSamples == 0) voice.active = false
|
||||||
|
g
|
||||||
|
} else 1.0
|
||||||
|
mixL += s * vol * lGain * rampGain
|
||||||
|
mixR += s * vol * rGain * rampGain
|
||||||
}
|
}
|
||||||
// Background (NNA-ghost) voices — same per-sample mixing path as foreground, but
|
// Background (NNA-ghost) voices — same per-sample mixing path as foreground, but
|
||||||
// they live in a mixer-private pool that no row event can address.
|
// they live in a mixer-private pool that no row event can address.
|
||||||
@@ -2714,14 +2755,24 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val lGain: Double
|
val lGain: Double
|
||||||
val rGain: Double
|
val rGain: Double
|
||||||
when (ts.panLaw) {
|
when (ts.panLaw) {
|
||||||
1 -> { lGain = cos(PI * pan / 512.0); rGain = sin(PI * pan / 512.0) }
|
1 -> {
|
||||||
|
lGain = cos(PI * pan / 512.0)
|
||||||
|
rGain = sin(PI * pan / 512.0)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
lGain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
|
lGain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
|
||||||
rGain = if (pan < 0x80) pan / 128.0 else 1.0
|
rGain = if (pan < 0x80) pan / 128.0 else 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mixL += s * vol * lGain
|
val rampGain = if (bg.rampOutSamples > 0) {
|
||||||
mixR += s * vol * rGain
|
val g = bg.rampOutGain
|
||||||
|
bg.rampOutGain -= bg.rampOutStep
|
||||||
|
bg.rampOutSamples--
|
||||||
|
if (bg.rampOutSamples == 0) bg.active = false
|
||||||
|
g
|
||||||
|
} else 1.0
|
||||||
|
mixL += s * vol * lGain * rampGain
|
||||||
|
mixR += s * vol * rGain * rampGain
|
||||||
}
|
}
|
||||||
|
|
||||||
ts.mixLeft[n] = mixL.toFloat().coerceIn(-1.0f, 1.0f)
|
ts.mixLeft[n] = mixL.toFloat().coerceIn(-1.0f, 1.0f)
|
||||||
@@ -2946,6 +2997,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// Volume fadeout — engaged after key-off, decays to 0 at rate inst.volumeFadeoutLow.
|
// Volume fadeout — engaged after key-off, decays to 0 at rate inst.volumeFadeoutLow.
|
||||||
var fadeoutVolume = 1.0
|
var fadeoutVolume = 1.0
|
||||||
|
|
||||||
|
// MilkyTracker-style anti-click ramp-out. Engaged when a sample naturally ends
|
||||||
|
// (loopMode 0/3 reaching sampleLen). Gain ramps from 1.0 → 0.0 over rampOutSamples
|
||||||
|
// while the held last-sample value keeps being emitted; voice deactivates at 0.
|
||||||
|
// Not engaged on note start — attack transients pass unsmoothed.
|
||||||
|
var rampOutSamples = 0
|
||||||
|
var rampOutGain = 0.0
|
||||||
|
var rampOutStep = 0.0
|
||||||
|
|
||||||
// Auto-vibrato (per-sample on the IT side, hoisted to the instrument here).
|
// Auto-vibrato (per-sample on the IT side, hoisted to the instrument here).
|
||||||
var autoVibPhase = 0 // 8-bit phase counter
|
var autoVibPhase = 0 // 8-bit phase counter
|
||||||
var autoVibTicksSinceTrigger = 0 // for sweep ramp-up
|
var autoVibTicksSinceTrigger = 0 // for sweep ramp-up
|
||||||
|
|||||||
Reference in New Issue
Block a user