audio device changes

This commit is contained in:
minjaesong
2026-04-16 15:04:44 +09:00
parent 2ac084acd7
commit 6aa2542bb8
11 changed files with 153 additions and 59 deletions

View File

@@ -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)

View File

@@ -0,0 +1,5 @@
/**
* Hopper is a package manager for TSVM
* Created by CuriousTorvald on 2026-04-16
*/

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 }

View File

@@ -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) {

View File

@@ -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<Playhead>
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()