per-voice fader to replace mute function

This commit is contained in:
minjaesong
2026-05-11 11:00:40 +09:00
parent a28fcbcefc
commit 2ffdf32c91
2 changed files with 52 additions and 12 deletions

View File

@@ -91,11 +91,22 @@ class AudioJSR223Delegate(private val vm: VM) {
fun getTrackerRow(playhead: Int) = getPlayhead(playhead)?.trackerState?.rowIndex ?: 0
/** Mute is now a thin wrapper over the per-voice fader: muting writes 255 (silence),
* unmuting clears the fader back to 0 (unity). Callers that want a partial attenuation
* should use setVoiceFader directly. */
fun setVoiceMute(playhead: Int, voice: Int, muted: Boolean) {
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.muted = muted
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.fader = if (muted) 255 else 0
}
fun getVoiceMute(playhead: Int, voice: Int): Boolean =
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.muted ?: false
(getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.fader ?: 0) == 255
/** Externally-controlled per-voice fader. 0 = unity, 255 = silence; values are masked to 8 bits.
* Mirrors MMIO 4098.. (256 bytes per playhead, first 20 entries map to live voice slots). */
fun setVoiceFader(playhead: Int, voice: Int, fader: Int) {
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.fader = fader and 255
}
fun getVoiceFader(playhead: Int, voice: Int): Int =
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.fader ?: 0
/** Set the starting row for the next play call, resetting per-row timing and silencing active voices. */
fun setTrackerRow(playhead: Int, row: Int) {

View File

@@ -455,6 +455,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
in 64..2367 -> mediaDecodedBin[addr - 64]
in 2368..4095 -> mediaFrameBin[addr - 2368]
in 4096..4097 -> 0
// Per-voice fader (0 = unity, 255 = silence): 256 bytes per playhead, only the first
// 20 entries map to live voice slots; the rest read 0.
in 4098..5121 -> {
val off = adi - 4098
val ph = off ushr 8 // playhead index 0..3
val v = off and 0xFF // voice index 0..255
if (v < 20) (playheads[ph].trackerState?.voices?.getOrNull(v)?.fader ?: 0).toByte()
else 0.toByte()
}
in 32768..65535 -> (adi - 32768).let {
cueSheet[it / 32].read(it % 32)
}
@@ -488,6 +497,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
45 -> selectedPcmBin = bi % 4
46 -> sampleBank = bi and SAMPLE_BANK_MASK
// Per-voice fader writes: see mmio_read for layout. Indices 20..255 are accepted
// but ignored so software can stride 256 bytes per playhead without bounds-checking.
in 4098..5121 -> {
val off = adi - 4098
val ph = off ushr 8
val v = off and 0xFF
if (v < 20) {
playheads[ph].trackerState?.voices?.getOrNull(v)?.fader = bi
}
}
in 64..2367 -> { mediaDecodedBin[addr - 64] = byte }
in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte }
in 32768..65535 -> { (adi - 32768).let {
@@ -2072,7 +2091,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
private fun ghostVoice(src: Voice, channel: Int): Voice {
val v = Voice()
v.active = true
v.muted = src.muted
v.fader = src.fader
v.instrumentId = src.instrumentId
v.samplePos = src.samplePos
v.playbackRate = src.playbackRate
@@ -3072,9 +3091,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val gvol = playhead.globalVolume / 255.0
val mvol = playhead.mixingVolume / 255.0
for (voice in ts.voices) {
if (!voice.active || voice.muted) {
// Keep the soundscope flat between notes / while muted so the AudioMenu
// does not show stale waveform data once the voice goes silent.
if (!voice.active || voice.fader == 255) {
// Keep the soundscope flat between notes / while fully faded (incl. host mute)
// so the AudioMenu does not show stale waveform data once the voice goes silent.
voice.scopeBuffer[voice.scopeWritePos] = 0f
voice.scopeWritePos = (voice.scopeWritePos + 1) and (SCOPE_BUFFER_SIZE - 1)
continue
@@ -3094,10 +3113,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// triggers snap currentMixVolume to target (in triggerNote) so attacks
// are passed through unramped.
advanceVolumeRamp(voice)
// External per-voice fader (0 = unity, 255 = silence). Folded into perVoiceGain
// so the soundscope reflects what the user hears after the fader is applied.
val faderGain = (255 - voice.fader) / 255.0
// Split the gain stack so the soundscope can see the voice amplitude independently
// of the playhead-wide faders (master / mixing / global volume).
val perVoiceGain = effEnvVol * voice.fadeoutVolume * voice.currentMixVolume *
swingScale * instGv
swingScale * instGv * faderGain
val globalGain = gvol * mvol * playhead.masterVolume / 255.0
val vol = perVoiceGain * globalGain
val pan = if (voice.hasPanEnv && voice.panEnvOn) {
@@ -3127,7 +3149,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// 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.
for (bg in ts.backgroundVoices) {
if (!bg.active || bg.muted) continue
if (!bg.active || bg.fader == 255) continue
val bgInst = instruments[bg.instrumentId]
val s = applyTaudVoiceFx(bg, applyVoiceFilter(bg, fetchTrackerSample(bg, bgInst, ts.interpolationMode)))
val instGv = bgInst.instGlobalVolume / 255.0
@@ -3138,8 +3160,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// can leave currentMixVolume mid-ramp from the foreground's last change —
// keep advancing so the inherited ramp completes cleanly.
advanceVolumeRamp(bg)
// External fader snapshotted at ghost time (see ghostVoice). Subsequent host
// changes to the source slot's fader don't affect already-ghosted voices.
val faderGain = (255 - bg.fader) / 255.0
val vol = effEnvVol * bg.fadeoutVolume * bg.currentMixVolume *
swingScale * gvol * mvol * instGv * playhead.masterVolume / 255.0
swingScale * gvol * mvol * instGv * faderGain * playhead.masterVolume / 255.0
val pan = if (bg.hasPanEnv && bg.panEnvOn) {
val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255)
(bg.channelPan + envPanRaw - 128 + bg.randomPanBias).coerceIn(0, 255)
@@ -3376,8 +3401,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
class Voice {
var active = false
var muted = false
var instrumentId = 0
// Externally-controlled 256-step attenuator (MMIO 4098.., AudioJSR223Delegate.setVoiceFader).
// 0 = unity, 255 = silence — and 255 is also the "mute" sentinel that setVoiceMute writes,
// so there is only one piece of host-owned per-voice state. Not touched by row events /
// tracker effects; survives note triggers because the host owns it. Cleared back to 0 only
// by resetParams() (full playhead reset).
var fader = 0
var samplePos = 0.0
var playbackRate = 1.0
var forward = true
@@ -3814,7 +3843,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
it.funkSpeed = 0
it.funkAccumulator = 0
it.funkWritePos = 0
it.muted = false
it.fader = 0
it.nnaOverride = -1
it.volEnvOn = true; it.panEnvOn = true; it.pfEnvOn = true
it.noteFading = false