From 6aa2542bb8b47efce84a349275810ee5bb3da76f Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 16 Apr 2026 15:04:44 +0900 Subject: [PATCH] audio device changes --- assets/disk0/home/playrawpcm.js | 4 +- assets/disk0/tvdos/bin/hopper.js | 5 ++ assets/disk0/tvdos/bin/playmv1.js | 2 +- assets/disk0/tvdos/bin/playpcm.js | 2 +- assets/disk0/tvdos/bin/playtav.js | 61 ++++++++--------- assets/disk0/tvdos/bin/playwav.js | 2 +- assets/disk0/tvdos/include/psg.mjs | 67 +++++++++++++++++-- terranmon.txt | 25 ++++--- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 23 ++++++- tsvm_core/src/net/torvald/tsvm/VM.kt | 2 +- .../torvald/tsvm/peripheral/AudioAdapter.kt | 19 ++++-- 11 files changed, 153 insertions(+), 59 deletions(-) create mode 100644 assets/disk0/tvdos/bin/hopper.js diff --git a/assets/disk0/home/playrawpcm.js b/assets/disk0/home/playrawpcm.js index 13786d1..98033f1 100644 --- a/assets/disk0/home/playrawpcm.js +++ b/assets/disk0/home/playrawpcm.js @@ -4,7 +4,7 @@ music.pread(samples, 65534) audio.setPcmMode(0) audio.setMasterVolume(0, 255) -audio.putPcmDataByPtr(samples, 65534, 0) +audio.putPcmDataByPtr(0, samples, 65534, 0) audio.setLoopPoint(0, 65534) audio.play(0)*/ @@ -127,7 +127,7 @@ while (sampleSize > 0) { let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE readBytes(readLength, decodePtr) - audio.putPcmDataByPtr(decodePtr, readLength, 0) + audio.putPcmDataByPtr(0, decodePtr, readLength, 0) audio.setSampleUploadLength(0, readLength) audio.startSampleUpload(0) diff --git a/assets/disk0/tvdos/bin/hopper.js b/assets/disk0/tvdos/bin/hopper.js new file mode 100644 index 0000000..8ec85a1 --- /dev/null +++ b/assets/disk0/tvdos/bin/hopper.js @@ -0,0 +1,5 @@ +/** + * Hopper is a package manager for TSVM + * Created by CuriousTorvald on 2026-04-16 + */ + diff --git a/assets/disk0/tvdos/bin/playmv1.js b/assets/disk0/tvdos/bin/playmv1.js index 7e43b79..955f76d 100644 --- a/assets/disk0/tvdos/bin/playmv1.js +++ b/assets/disk0/tvdos/bin/playmv1.js @@ -326,7 +326,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) { // RAW PCM packets (decode on the fly) else if (packetType == 0x1000 || packetType == 0x1001) { let frame = seqread.readBytes(readLength) - audio.putPcmDataByPtr(frame, readLength, 0) + audio.putPcmDataByPtr(0, frame, readLength, 0) audio.setSampleUploadLength(0, readLength) audio.startSampleUpload(0) sys.free(frame) diff --git a/assets/disk0/tvdos/bin/playpcm.js b/assets/disk0/tvdos/bin/playpcm.js index cc82ae2..e91565e 100644 --- a/assets/disk0/tvdos/bin/playpcm.js +++ b/assets/disk0/tvdos/bin/playpcm.js @@ -162,7 +162,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) { seqread.readBytes(readLength, readPtr) - audio.putPcmDataByPtr(readPtr, readLength, 0) + audio.putPcmDataByPtr(0, readPtr, readLength, 0) audio.setSampleUploadLength(0, readLength) audio.startSampleUpload(0) diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index e00315a..92342b9 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -18,6 +18,7 @@ const ADDRESSING_INTERNAL = 0x02 const SND_BASE_ADDR = audio.getBaseAddr() const SND_MEM_ADDR = audio.getMemAddr() const pcm = require("pcm") +const AUDIO_DEVICE = 3 const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728] const TAV_TEMPORAL_LEVELS = 2 @@ -152,10 +153,10 @@ graphics.clearPixels4(0) const gpuGraphicsMode = graphics.getGraphicsMode() // Initialize audio -audio.resetParams(0) -audio.purgeQueue(0) -audio.setPcmMode(0) -audio.setMasterVolume(0, 255) +audio.resetParams(AUDIO_DEVICE) +audio.purgeQueue(AUDIO_DEVICE) +audio.setPcmMode(AUDIO_DEVICE) +audio.setMasterVolume(AUDIO_DEVICE, 255) // set colour zero as half-opaque black graphics.setPalette(0, 0, 0, 0, 7) @@ -1152,10 +1153,10 @@ try { else if (keyCode == 62) { // SPACE - pause/resume paused = !paused if (paused) { - audio.stop(0) + audio.stop(AUDIO_DEVICE) serial.println(`Paused at frame ${frameCount}`) } else { - audio.play(0) + audio.play(AUDIO_DEVICE) serial.println(`Resumed`) } } @@ -1176,10 +1177,10 @@ try { baseTimecodeFrameCount = 0 currentTimecodeNs = 0 nextSubtitleEventIndex = 0 // Reset subtitle event processing - audio.purgeQueue(0) + audio.purgeQueue(AUDIO_DEVICE) if (paused) { - audio.play(0) - audio.stop(0) + audio.play(AUDIO_DEVICE) + audio.stop(AUDIO_DEVICE) } skipped = true } @@ -1201,10 +1202,10 @@ try { baseTimecodeFrameCount = 0 currentTimecodeNs = 0 nextSubtitleEventIndex = 0 // Reset subtitle event processing - audio.purgeQueue(0) + audio.purgeQueue(AUDIO_DEVICE) if (paused) { - audio.play(0) - audio.stop(0) + audio.play(AUDIO_DEVICE) + audio.stop(AUDIO_DEVICE) } skipped = true } @@ -1232,10 +1233,10 @@ try { break } } - audio.purgeQueue(0) + audio.purgeQueue(AUDIO_DEVICE) if (paused) { - audio.play(0) - audio.stop(0) + audio.play(AUDIO_DEVICE) + audio.stop(AUDIO_DEVICE) } skipped = true } @@ -1271,10 +1272,10 @@ try { break } } - audio.purgeQueue(0) + audio.purgeQueue(AUDIO_DEVICE) if (paused) { - audio.play(0) - audio.stop(0) + audio.play(AUDIO_DEVICE) + audio.stop(AUDIO_DEVICE) } skipped = true } else if (!seekTarget) { @@ -1313,7 +1314,7 @@ try { baseTimecodeFrameCount = 0 currentTimecodeNs = 0 nextSubtitleEventIndex = 0 // Reset subtitle event processing - audio.purgeQueue(0) + audio.purgeQueue(AUDIO_DEVICE) currentFileIndex++ if (skipped) { skipped = false @@ -1737,7 +1738,7 @@ try { seqread.readBytes(audioLen, SND_BASE_ADDR - 2368) audio.mp2Decode() - audio.mp2UploadDecoded(0) + audio.mp2UploadDecoded(AUDIO_DEVICE) } else if (packetType === TAV_PACKET_AUDIO_TAD) { @@ -1750,7 +1751,7 @@ try { seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144) audio.tadDecode() - audio.tadUploadDecoded(0, sampleLen) + audio.tadUploadDecoded(AUDIO_DEVICE, sampleLen) } else if (packetType === TAV_PACKET_AUDIO_NATIVE) { // PCM length must not exceed 65536 bytes! @@ -1762,10 +1763,10 @@ try { let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, pcmPtr) // <- segfaults! if (pcmLen > 65536) throw Error(`PCM data too long -- got ${pcmLen} bytes`) - audio.putPcmDataByPtr(pcmPtr, pcmLen, 0) + audio.putPcmDataByPtr(AUDIO_DEVICE, pcmPtr, pcmLen, 0) - audio.setSampleUploadLength(0, pcmLen) - audio.startSampleUpload(0) + audio.setSampleUploadLength(AUDIO_DEVICE, pcmLen) + audio.startSampleUpload(AUDIO_DEVICE) sys.free(zstdPtr) sys.free(pcmPtr) @@ -2049,7 +2050,7 @@ try { // Fire audio on first frame if (!audioFired) { - audio.play(0) + audio.play(AUDIO_DEVICE) audioFired = true } @@ -2137,7 +2138,7 @@ try { // Fire audio on first frame if (!audioFired) { - audio.play(0) + audio.play(AUDIO_DEVICE) audioFired = true } @@ -2173,8 +2174,8 @@ try { sys.memcpy(predecodedPcmBuffer + predecodedPcmOffset, SND_BASE_ADDR, uploadSize) // Set upload parameters and trigger upload to queue - audio.setSampleUploadLength(0, uploadSize) - audio.startSampleUpload(0) + audio.setSampleUploadLength(AUDIO_DEVICE, uploadSize) + audio.startSampleUpload(AUDIO_DEVICE) predecodedPcmOffset += uploadSize } @@ -2458,8 +2459,8 @@ finally { sys.poke(-1299460, 20) sys.poke(-1299460, 21) - audio.stop(0) - audio.purgeQueue(0) + audio.stop(AUDIO_DEVICE) + audio.purgeQueue(AUDIO_DEVICE) } graphics.setPalette(0, 0, 0, 0, 0) diff --git a/assets/disk0/tvdos/bin/playwav.js b/assets/disk0/tvdos/bin/playwav.js index 6c9ae6d..59f0add 100644 --- a/assets/disk0/tvdos/bin/playwav.js +++ b/assets/disk0/tvdos/bin/playwav.js @@ -289,7 +289,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE - 8) { let decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength) printdbg(` decodedSampleLength: ${decodedSampleLength}`) - audio.putPcmDataByPtr(decodePtr, decodedSampleLength, 0) + audio.putPcmDataByPtr(0, decodePtr, decodedSampleLength, 0) audio.setSampleUploadLength(0, decodedSampleLength) audio.startSampleUpload(0) diff --git a/assets/disk0/tvdos/include/psg.mjs b/assets/disk0/tvdos/include/psg.mjs index 85ef209..e66eeba 100644 --- a/assets/disk0/tvdos/include/psg.mjs +++ b/assets/disk0/tvdos/include/psg.mjs @@ -292,6 +292,24 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) { } } +function makeAliasedTriangleNES(buf, length, offset, freq, duty, op, amp, pan) { + // NES APU triangle — quantised to the authentic 32-step, 4-bit (0..15) staircase. + // The 32-step sequence is: 15,14,...,1,0, 0,1,...,14,15 (descending then ascending). + // This mirrors the real NES triangle DAC which has 32 equal-height steps per period. + // duty parameter is accepted for API symmetry but ignored (NES triangle is always symmetric). + if (op == null) op = 'add' + if (amp == null) amp = 0.5 + if (pan == null) pan = 0.0 + mixInto(buf, length, offset, op, amp, pan, function(i) { + const t = offset + i / HW_SAMPLING_RATE + const phase = (t * freq) % 1 + const step32 = Math.floor(phase * 32) | 0 // 0..31 + // step 0..15: descend from 15 to 0; step 16..31: ascend from 0 to 15 + const level = (step32 < 16) ? (15 - step32) : (step32 - 16) + return level / 7.5 - 1.0 // map 0..15 → -1..+1 + }) +} + // ── Send to audio hardware ────────────────────────────────────────────────── function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) { @@ -322,8 +340,49 @@ function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) { sys.poke(stagingPtr + 2 * i + 1, readU8(buf, 1, cursor + i)) } // Wait for room in the playback queue (mirrors playwav.js idiom) - while (audio.getPosition(playhead) > 2) sys.sleep(2) - audio.putPcmDataByPtr(stagingPtr, take * 2, 0) + // while (audio.getPosition(playhead) > 2) sys.sleep(2) + audio.putPcmDataByPtr(stagingPtr, take * 2, playhead) + audio.setSampleUploadLength(playhead, take * 2) + audio.startSampleUpload(playhead) + remaining -= take + cursor += take + } + + if (ownsStaging) sys.free(stagingPtr) +} + +// Lazily-allocated JS-side interleave scratch; shared across sendBufferFast calls. +let _sendFastScratch = null + +function sendBufferFast(buf, playhead, offsetSec, lengthSec, stagingPtr) { + // Like sendBuffer but interleaves L/R via a JS Uint8Array + one sys.pokeBytes per chunk, + // instead of ~2n sys.poke calls. Requires a non-native (JS-backed) buffer. + // Falls back to sendBuffer for native buffers. + if (isNative(buf)) { sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr); return } + + const start = (offsetSec != null) ? secToSamples(offsetSec) : 0 + const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start) + const MAX_CHUNK = 32768 + const ownsStaging = (stagingPtr == null) + if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2) + + const scratchNeeded = Math.min(total, MAX_CHUNK) * 2 + if (_sendFastScratch == null || _sendFastScratch.length < scratchNeeded) { + _sendFastScratch = new Uint8Array(scratchNeeded) + } + + let remaining = total + let cursor = start + while (remaining > 0) { + const take = Math.min(remaining, MAX_CHUNK) + const L = buf[0], R = buf[1], sc = _sendFastScratch + for (let i = 0; i < take; i++) { + sc[2 * i] = L[cursor + i] + sc[2 * i + 1] = R[cursor + i] + } + sys.pokeBytes(stagingPtr, sc.subarray(0, take * 2), take * 2) + // while (audio.getPosition(playhead) > 2) sys.sleep(2) + audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0) audio.setSampleUploadLength(playhead, take * 2) audio.startSampleUpload(playhead) remaining -= take @@ -336,6 +395,6 @@ function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) { exports = { HW_SAMPLING_RATE, makeBuffer, makeBufferNative, freeBufferNative, clearBuffer, - makeSquare, makeTriangle, makeAliasedTriangle, makeNoise, - sendBuffer + makeSquare, makeTriangle, makeAliasedTriangle, makeAliasedTriangleNES, makeNoise, + sendBuffer, sendBufferFast } diff --git a/terranmon.txt b/terranmon.txt index 2ddfd39..f1b2513 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2014,17 +2014,17 @@ notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using t Sound Adapter MMIO -0..1 RW: Play head #1 position -2..3 RW: Play head #1 length param -4 RW: Play head #1 master volume -5 RW: Play head #1 master pan -6..9 RW: Play head #1 flags +0..1 RW: Play head #0 position (how many samples has been queued) +2..3 RW: Play head #0 length param +4 RW: Play head #0 master volume +5 RW: Play head #0 master pan +6..9 RW: Play head #0 flags -10..11 RW:Play head #2 position -12..13 RW:Play head #2 length param -14 RW: Play head #2 master volume -15 RW: Play head #2 master pan -16..19 RW:Play head #2 flags +10..11 RW:Play head #1 position (how many samples has been queued) +12..13 RW:Play head #1 length param +14 RW: Play head #1 master volume +15 RW: Play head #1 master pan +16..19 RW:Play head #1 flags ... auto-fill to Play head #4 @@ -2034,13 +2034,16 @@ Sound Adapter MMIO When called with byte 17, initialisation will precede before the decoding -41 RO: Media Decoder Status +41 RO: MP2 Decoder Status Non-zero value indicates the decoder is busy 42 WO: TAD Decoder Control Write 1 to decode TAD data 43 RW: TAD Quality Must be set to appropriate value before decoding +44 RW: TAD Decoder Status + +45 RW: Select PCM Bin for playhead (writing causes side effects) 64..2367 RW: MP2 Decoded Samples (unsigned 8-bit stereo) 2368..4095 RW: MP2 Frame to be decoded diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 64d05ac..70e5dce 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -5,6 +5,23 @@ import net.torvald.tsvm.peripheral.AudioAdapter import net.torvald.tsvm.peripheral.MP2Env /** + * Each playhead is separate OpenAL device with its own PCM sample buffers. + * Media decoders (MP2, TAD) are independent to the playheads and there is only one. + * + * NOTES: + * 1. tracker mode is currently unimplemented. + * 2. PCM upload buffer (accessed by `putPcmDataByPtr`) is shared between four playheads + * + * ## How to upload PCM audio into a playhead + * + * 1. prepare PCM data + * 2. queue up PCM data by `audio.putPcmDataByPtr(pcmDataPtr, pcmDataLength, playhead)` + * 3. specify PCM upload length by `audio.setSampleUploadLength(playhead, pcmDataLength)` + * 4. start uploading `audio.startSampleUpload(playhead)` + * 5. sample will be ready after a few microseconds. + * + * Uploaded samples will be queued by the playhead for gapless playback + * * Created by minjaesong on 2022-12-31. */ class AudioJSR223Delegate(private val vm: VM) { @@ -51,16 +68,16 @@ class AudioJSR223Delegate(private val vm: VM) { fun setTickRate(playhead: Int, rate: Int) { getPlayhead(playhead)?.tickRate = rate and 255 } fun getTickRate(playhead: Int) = getPlayhead(playhead)?.tickRate - fun putPcmDataByPtr(ptr: Int, length: Int, destOffset: Int) { + fun putPcmDataByPtr(playhead: Int, ptr: Int, length: Int, destOffset: Int) { getFirstSnd()?.let { val vkMult = if (ptr >= 0) 1 else -1 for (k in 0L until length) { val vk = k * vkMult - it.pcmBin[k + destOffset] = vm.peek(ptr + vk)!! + it.pcmBin[playhead][k + destOffset] = vm.peek(ptr + vk)!! } } } - fun getPcmData(index: Int) = getFirstSnd()?.pcmBin?.get(index.toLong()) + fun getPcmData(playhead: Int, index: Int) = getFirstSnd()?.pcmBin?.get(playhead)?.get(index.toLong()) fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index } fun getPcmQueueCapacityIndex(playhead: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex } diff --git a/tsvm_core/src/net/torvald/tsvm/VM.kt b/tsvm_core/src/net/torvald/tsvm/VM.kt index 6c130b9..404a755 100644 --- a/tsvm_core/src/net/torvald/tsvm/VM.kt +++ b/tsvm_core/src/net/torvald/tsvm/VM.kt @@ -763,7 +763,7 @@ class VM( else if (dev is AudioAdapter) { if (relPtrInDev(fromRel, len, 64, 2367)) dev.mediaDecodedBin.ptr + fromRel - 64 else if (relPtrInDev(fromRel, len, 2368, 4096)) dev.mediaFrameBin.ptr + fromRel - 2368 - else if (relPtrInDev(fromRel, len, 65536, 131072)) dev.pcmBin.ptr + fromRel - 65536 + else if (relPtrInDev(fromRel, len, 65536, 131072)) dev.pcmBin[dev.selectedPcmBin].ptr + fromRel - 65536 else null } else if (dev is GraphicsAdapter) { diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 71cad24..bb3c765 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -122,13 +122,20 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { internal val playdata = Array(256) { Array(64) { TaudPlayData(0,0,0,0,0,0,0,0) } } internal val playheads: Array internal val cueSheet = Array(2048) { PlayCue() } - internal val pcmBin = UnsafeHelper.allocate(65536L, this) + internal val pcmBin = arrayOf( + UnsafeHelper.allocate(65536L, this), + UnsafeHelper.allocate(65536L, this), + UnsafeHelper.allocate(65536L, this), + UnsafeHelper.allocate(65536L, this), + ) internal val mediaFrameBin = UnsafeHelper.allocate(1728, this) internal val mediaDecodedBin = UnsafeHelper.allocate(2304, this) @Volatile private var mp2Busy = false + @Volatile var selectedPcmBin = 0 + // TAD (Terrarum Advanced Audio) decoder buffers internal val tadInputBin = UnsafeHelper.allocate(65536L, this) // Input: compressed TAD chunk (max 64KB) internal val tadDecodedBin = UnsafeHelper.allocate(65536L, this) // Output: PCMu8 stereo (32768 samples * 2 channels) @@ -241,7 +248,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { renderRunnables = Array(4) { RenderRunnable(playheads[it]) } renderThreads = Array(4) { Thread(renderThreadGroup, renderRunnables[it], "AudioRenderHead${it+1}!$hash") } - writeQueueingRunnables = Array(4) { WriteQueueingRunnable(playheads[it], pcmBin) } + writeQueueingRunnables = Array(4) { WriteQueueingRunnable(playheads[it], pcmBin[it]) } writeQueueingThreads = Array(4) { Thread(writeQueueingGroup, writeQueueingRunnables[it], "AudioQueueingHead${it+1}!$hash") } // printdbg("AudioAdapter latency: ${audioDevice.latency}") @@ -315,13 +322,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { 42 -> -1 // TAD control (write-only) 43 -> tadQuality.toByte() 44 -> tadBusy.toInt().toByte() + 45 -> selectedPcmBin.toByte() in 64..2367 -> mediaDecodedBin[addr - 64] in 2368..4095 -> mediaFrameBin[addr - 2368] in 4096..4097 -> 0 in 32768..65535 -> (adi - 32768).let { cueSheet[it / 16].read(it % 15) } - in 65536..131071 -> pcmBin[addr - 65536] + in 65536..131071 -> pcmBin[selectedPcmBin][addr - 65536] else -> { println("[AudioAdapter] Bus mirroring on mmio_reading while trying to read address $addr") mmio_read(addr % 131072) @@ -349,12 +357,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // TAD quality (0-5) tadQuality = bi.coerceIn(0, 5) } + 45 -> selectedPcmBin = bi % 4 in 64..2367 -> { mediaDecodedBin[addr - 64] = byte } in 2368..4095 -> { mediaFrameBin[addr - 2368] = byte } in 32768..65535 -> { (adi - 32768).let { cueSheet[it / 16].write(it % 15, bi) } } - in 65536..131071 -> { pcmBin[addr - 65536] = byte } + in 65536..131071 -> { pcmBin[selectedPcmBin][addr - 65536] = byte } } } @@ -368,7 +377,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { writeQueueingGroup.interrupt() playheads.forEach { it.dispose() } sampleBin.destroy() - pcmBin.destroy() + pcmBin.forEach { it.destroy() } mediaFrameBin.destroy() mediaDecodedBin.destroy() tadInputBin.destroy()