volume policy when unspecified: retrigger (note+inst cmd) -> default value, no retrigger (note cmd only) -> prev value

This commit is contained in:
minjaesong
2026-05-08 01:11:19 +09:00
parent a767eebc2e
commit 34b3b83d65
3 changed files with 39 additions and 22 deletions

View File

@@ -1826,8 +1826,10 @@ function simulateRowState(ptnDat, uptoRow) {
const isGRow = (effop === OP_G)
const isNoteDelay = (effop === OP_S) && (((effarg >>> 12) & 0xF) === 0xD)
// Track whether this row reloads the channel's default volume. Engine:
// triggerNote() resets channelVolume to 0x3F on fresh triggers, and an
// instrument byte on a tone-porta row also reloads default vol (matches
// triggerNote() resets channelVolume to 0x3F only when the row carries an
// instrument byte; a note-only retrigger (inst === 0) inherits the
// channel's existing volume. Tone-porta rows follow the same rule —
// an instrument byte on a porta row reloads default vol (matches
// schism csf_instrument_change inst_column branch).
let reloadDefaultVol = false
if (note !== 0xFFFF && note !== 0xFFFE) {
@@ -1842,17 +1844,21 @@ function simulateRowState(ptnDat, uptoRow) {
lastNote = note
pitchOff = 0
portaTarget = -1
reloadDefaultVol = true
if (inst !== 0) reloadDefaultVol = true
} else {
lastNote = note
pitchOff = 0
portaTarget = -1
reloadDefaultVol = true
if (inst !== 0) reloadDefaultVol = true
}
}
if (inst !== 0) lastInst = inst
// Default vol reset must happen before the volume column so a SET selector
// can still override on the same row (engine order: triggerNote → applyVolColumn).
// Pan: simulator does not track per-instrument default pan, so it never resets
// panAbs on trigger — this naturally matches the "stay at old value when inst === 0"
// half of the policy. The engine-side default-pan reload (gated on inst !== 0)
// is invisible here.
if (reloadDefaultVol) volAbs = 0x3F
// Pre-scan effect column for S$80xx (8-bit pan SET wins over volcol/pancol SET).

View File

@@ -2345,7 +2345,7 @@ TODO:
for LEFT/RIGHT and `shiftPatternArea` for UP/DOWN, plus per-row
(`drawOrdersRowAt`) and per-column (`drawOrdersVoiceColumnAt`) helpers,
replacing the full-panel redraw on every keystroke.
[ ] 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.
Play Data: play data are series of tracker-like instructions, visualised as:

View File

@@ -1689,19 +1689,24 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
(Math.random() * (2 * inst.volumeSwing + 1)).toInt() - inst.volumeSwing else 0
voice.randomPanBias = if (inst.panSwing != 0)
(Math.random() * (2 * inst.panSwing + 1)).toInt() - inst.panSwing else 0
// Default pan: applied unless the pattern row has already overridden channelPan.
// The pan envelope's 'p' flag ("use default pan") lives in the pan LOOP word at bit 7.
if ((inst.panEnvLoop ushr 7) and 1 != 0) {
voice.channelPan = inst.defaultPan
voice.rowPan = (voice.channelPan ushr 2).coerceIn(0, 63)
}
// Pitch-pan separation: when PPS != 0, played notes far from PPC drift in pan.
// PPS is signed (-32..+32), full-scale at one octave (4096 4096-TET units) above PPC.
if (inst.pitchPanSeparation != 0) {
val noteDelta = (noteVal - inst.pitchPanCentre).toDouble() / 4096.0
val panShift = (noteDelta * inst.pitchPanSeparation * 4.0).toInt() // ~×4 = 32→128 swing
voice.channelPan = (voice.channelPan + panShift).coerceIn(0, 255)
voice.rowPan = (voice.channelPan ushr 2).coerceIn(0, 63)
// Default pan / pitch-pan separation: only re-applied when the row carried an instrument
// byte. A note-only retrigger (instId == 0) inherits the channel's existing pan, mirroring
// the volume policy below.
if (instId != 0) {
// Default pan: applied unless the pattern row has already overridden channelPan.
// The pan envelope's 'p' flag ("use default pan") lives in the pan LOOP word at bit 7.
if ((inst.panEnvLoop ushr 7) and 1 != 0) {
voice.channelPan = inst.defaultPan
voice.rowPan = (voice.channelPan ushr 2).coerceIn(0, 63)
}
// Pitch-pan separation: when PPS != 0, played notes far from PPC drift in pan.
// PPS is signed (-32..+32), full-scale at one octave (4096 4096-TET units) above PPC.
if (inst.pitchPanSeparation != 0) {
val noteDelta = (noteVal - inst.pitchPanCentre).toDouble() / 4096.0
val panShift = (noteDelta * inst.pitchPanSeparation * 4.0).toInt() // ~×4 = 32→128 swing
voice.channelPan = (voice.channelPan + panShift).coerceIn(0, 255)
voice.rowPan = (voice.channelPan ushr 2).coerceIn(0, 63)
}
}
// Filter cutoff/resonance defaults — adjusted per-tick by the pf envelope when in filter mode.
// 255 = filter off (IT high-bit-clear); 0..254 = active range matching IT 0..127 at double resolution.
@@ -1715,10 +1720,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.amigaPeriod = -1.0 // fresh trigger: period state must reseed from the new noteVal
voice.linearFreq = -1.0 // ditto for linear-freq mode (toneMode == 2)
voice.playbackRate = computePlaybackRate(inst, noteVal)
// 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
// Fresh trigger resets channel volume to full ($3F) ONLY when the row carried an
// instrument byte; a note-only retrigger (instId == 0) inherits the channel's existing
// volume so the user can sustain a held volume across re-triggered notes. 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 = when {
volOverride >= 0 -> volOverride.coerceIn(0, 0x3F)
instId != 0 -> 0x3F
else -> voice.channelVolume
}
voice.rowVolume = voice.channelVolume
voice.noteWasCut = false
voice.noteFading = false