mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
per-voice fader to replace mute function
This commit is contained in:
@@ -91,11 +91,22 @@ class AudioJSR223Delegate(private val vm: VM) {
|
|||||||
|
|
||||||
fun getTrackerRow(playhead: Int) = getPlayhead(playhead)?.trackerState?.rowIndex ?: 0
|
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) {
|
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 =
|
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. */
|
/** Set the starting row for the next play call, resetting per-row timing and silencing active voices. */
|
||||||
fun setTrackerRow(playhead: Int, row: Int) {
|
fun setTrackerRow(playhead: Int, row: Int) {
|
||||||
|
|||||||
@@ -455,6 +455,15 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
in 64..2367 -> mediaDecodedBin[addr - 64]
|
in 64..2367 -> mediaDecodedBin[addr - 64]
|
||||||
in 2368..4095 -> mediaFrameBin[addr - 2368]
|
in 2368..4095 -> mediaFrameBin[addr - 2368]
|
||||||
in 4096..4097 -> 0
|
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 {
|
in 32768..65535 -> (adi - 32768).let {
|
||||||
cueSheet[it / 32].read(it % 32)
|
cueSheet[it / 32].read(it % 32)
|
||||||
}
|
}
|
||||||
@@ -488,6 +497,16 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
45 -> selectedPcmBin = bi % 4
|
45 -> selectedPcmBin = bi % 4
|
||||||
46 -> sampleBank = bi and SAMPLE_BANK_MASK
|
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 64..2367 -> { mediaDecodedBin[addr - 64] = byte }
|
||||||
in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte }
|
in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte }
|
||||||
in 32768..65535 -> { (adi - 32768).let {
|
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 {
|
private fun ghostVoice(src: Voice, channel: Int): Voice {
|
||||||
val v = Voice()
|
val v = Voice()
|
||||||
v.active = true
|
v.active = true
|
||||||
v.muted = src.muted
|
v.fader = src.fader
|
||||||
v.instrumentId = src.instrumentId
|
v.instrumentId = src.instrumentId
|
||||||
v.samplePos = src.samplePos
|
v.samplePos = src.samplePos
|
||||||
v.playbackRate = src.playbackRate
|
v.playbackRate = src.playbackRate
|
||||||
@@ -3072,9 +3091,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val gvol = playhead.globalVolume / 255.0
|
val gvol = playhead.globalVolume / 255.0
|
||||||
val mvol = playhead.mixingVolume / 255.0
|
val mvol = playhead.mixingVolume / 255.0
|
||||||
for (voice in ts.voices) {
|
for (voice in ts.voices) {
|
||||||
if (!voice.active || voice.muted) {
|
if (!voice.active || voice.fader == 255) {
|
||||||
// Keep the soundscope flat between notes / while muted so the AudioMenu
|
// Keep the soundscope flat between notes / while fully faded (incl. host mute)
|
||||||
// does not show stale waveform data once the voice goes silent.
|
// so the AudioMenu does not show stale waveform data once the voice goes silent.
|
||||||
voice.scopeBuffer[voice.scopeWritePos] = 0f
|
voice.scopeBuffer[voice.scopeWritePos] = 0f
|
||||||
voice.scopeWritePos = (voice.scopeWritePos + 1) and (SCOPE_BUFFER_SIZE - 1)
|
voice.scopeWritePos = (voice.scopeWritePos + 1) and (SCOPE_BUFFER_SIZE - 1)
|
||||||
continue
|
continue
|
||||||
@@ -3094,10 +3113,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// triggers snap currentMixVolume to target (in triggerNote) so attacks
|
// triggers snap currentMixVolume to target (in triggerNote) so attacks
|
||||||
// are passed through unramped.
|
// are passed through unramped.
|
||||||
advanceVolumeRamp(voice)
|
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
|
// Split the gain stack so the soundscope can see the voice amplitude independently
|
||||||
// of the playhead-wide faders (master / mixing / global volume).
|
// of the playhead-wide faders (master / mixing / global volume).
|
||||||
val perVoiceGain = effEnvVol * voice.fadeoutVolume * voice.currentMixVolume *
|
val perVoiceGain = effEnvVol * voice.fadeoutVolume * voice.currentMixVolume *
|
||||||
swingScale * instGv
|
swingScale * instGv * faderGain
|
||||||
val globalGain = gvol * mvol * playhead.masterVolume / 255.0
|
val globalGain = gvol * mvol * playhead.masterVolume / 255.0
|
||||||
val vol = perVoiceGain * globalGain
|
val vol = perVoiceGain * globalGain
|
||||||
val pan = if (voice.hasPanEnv && voice.panEnvOn) {
|
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
|
// 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.
|
||||||
for (bg in ts.backgroundVoices) {
|
for (bg in ts.backgroundVoices) {
|
||||||
if (!bg.active || bg.muted) continue
|
if (!bg.active || bg.fader == 255) continue
|
||||||
val bgInst = instruments[bg.instrumentId]
|
val bgInst = instruments[bg.instrumentId]
|
||||||
val s = applyTaudVoiceFx(bg, applyVoiceFilter(bg, fetchTrackerSample(bg, bgInst, ts.interpolationMode)))
|
val s = applyTaudVoiceFx(bg, applyVoiceFilter(bg, fetchTrackerSample(bg, bgInst, ts.interpolationMode)))
|
||||||
val instGv = bgInst.instGlobalVolume / 255.0
|
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 —
|
// can leave currentMixVolume mid-ramp from the foreground's last change —
|
||||||
// keep advancing so the inherited ramp completes cleanly.
|
// keep advancing so the inherited ramp completes cleanly.
|
||||||
advanceVolumeRamp(bg)
|
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 *
|
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 pan = if (bg.hasPanEnv && bg.panEnvOn) {
|
||||||
val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255)
|
val envPanRaw = (bg.envPan * 255.0).roundToInt().coerceIn(0, 255)
|
||||||
(bg.channelPan + envPanRaw - 128 + bg.randomPanBias).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 {
|
class Voice {
|
||||||
var active = false
|
var active = false
|
||||||
var muted = false
|
// Externally-controlled 256-step attenuator (MMIO 4098.., AudioJSR223Delegate.setVoiceFader).
|
||||||
var instrumentId = 0
|
// 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 samplePos = 0.0
|
||||||
var playbackRate = 1.0
|
var playbackRate = 1.0
|
||||||
var forward = true
|
var forward = true
|
||||||
@@ -3814,7 +3843,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
it.funkSpeed = 0
|
it.funkSpeed = 0
|
||||||
it.funkAccumulator = 0
|
it.funkAccumulator = 0
|
||||||
it.funkWritePos = 0
|
it.funkWritePos = 0
|
||||||
it.muted = false
|
it.fader = 0
|
||||||
it.nnaOverride = -1
|
it.nnaOverride = -1
|
||||||
it.volEnvOn = true; it.panEnvOn = true; it.pfEnvOn = true
|
it.volEnvOn = true; it.panEnvOn = true; it.pfEnvOn = true
|
||||||
it.noteFading = false
|
it.noteFading = false
|
||||||
|
|||||||
Reference in New Issue
Block a user