mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
volume policy when unspecified: retrigger (note+inst cmd) -> default value, no retrigger (note cmd only) -> prev value
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user