wav direct upload with bugfixes

This commit is contained in:
minjaesong
2026-06-20 17:27:27 +09:00
parent 04aa651ff1
commit 646af9452c
11 changed files with 228 additions and 68 deletions

View File

@@ -485,6 +485,28 @@ class AudioJSR223Delegate(private val vm: VM) {
}
}
}
/** Synchronously copy `length` bytes of PCMu8-stereo from `ptr` and enqueue them for playback,
* directly — like [mp2UploadDecoded]. The putPcmDataByPtr + setSampleUploadLength +
* startSampleUpload path hands off through the single-slot pcmBin/pcmUpload handshake serviced
* by WriteQueueingRunnable, which DROPS chunks when a caller queues several in a row (the
* next putPcmData overwrites pcmBin / clobbers pcmUploadLength before the thread copies it).
* Lost chunks make WAV/PCM playback skip and effectively fast-forward. Enqueue with no race. */
fun queuePcmDataByPtr(playhead: Int, ptr: Int, length: Int) {
if (length <= 0) return
val snd = getFirstSnd() ?: return
val ph = snd.playheads.getOrNull(playhead) ?: return
val ba = ByteArray(length)
if (ptr >= 0) {
// user RAM — fast bulk copy
UnsafeHelper.memcpyRaw(null, vm.usermem.ptr + ptr, ba, UnsafeHelper.getArrayOffset(ba), length.toLong())
} else {
// peripheral memory grows toward 0 — read backwards, like putPcmDataByPtr
for (k in 0 until length) ba[k] = vm.peek(ptr.toLong() - k.toLong())!!
}
ph.pcmQueue.add(ba)
ph.position = ph.pcmQueue.size
}
fun getPcmData(playhead: Int, index: Int) = getFirstSnd()?.pcmBin?.get(playhead)?.get(index.toLong())
fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index }

View File

@@ -25,6 +25,11 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
private fun printdbg(msg: Any) {
if (AudioAdapter.DBGPRN) println("[AudioAdapter] $msg")
}
// Diagnostic: effective PCM playback rate. Since writeStereoSamplesUI8 is paced by the device
// (fillBuffer blocks until OpenAL frees a buffer), frames-fed / wall-time == the device's real
// consumption rate. Should read ~32000; ~64000 would mean the device is playing 2x.
private var probeFrames = 0L
private var probeT0 = 0L
override fun run() {
while (!Thread.currentThread().isInterrupted) {
try {
@@ -43,7 +48,19 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
playhead.position = writeQueue.size
playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size)
// PCM queue data is stereo-interleaved PCMu8 (L,R,L,R) — WAV decode,
// MP2 mediaDecodedBin, TAD. Each frame is 2 bytes, so numPairs = size/2.
playhead.audioDevice.writeStereoSamplesUI8(samples, 0, samples.size / 2)
if (AudioAdapter.PCM_RATE_PROBE) {
if (probeT0 == 0L) probeT0 = System.nanoTime()
probeFrames += samples.size / 2
val dt = (System.nanoTime() - probeT0) / 1.0e9
if (dt >= 2.0) {
System.err.println("[AudioAdapter] P${playhead.index} PCM effective rate = ${(probeFrames / dt).toInt()} frames/s (expect 32000)")
probeFrames = 0L; probeT0 = System.nanoTime()
}
}
Thread.sleep(6)
}
@@ -129,6 +146,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
companion object {
internal val DBGPRN = false
// Set true to log the effective PCM playback rate to stderr (~every 2s) — should read
// ~32000 frames/s. Diagnosed the "WAV plays 2x faster" report: rate was correct (32000),
// so the cause was dropped queue chunks (handshake), now fixed via queuePcmDataByPtr.
internal val PCM_RATE_PROBE = false
const val SAMPLING_RATE = 32000
const val TRACKER_CHUNK = 512
// Per-voice soundscope ring-buffer length. Power of two so wrap-around is a single AND.
@@ -457,7 +478,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// printdbg("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...")
// printdbg(""+(0..42).joinToString { String.format("%.2f", samples[it]) })
playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size)
playhead.audioDevice.writeStereoSamplesUI8(samples, 0, samples.size / 2)
// printdbg("P${playhead.index+1} go back to spinning")

View File

@@ -139,7 +139,15 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
try {
val buffer = ByteArray(BLOCK_SIZE)
val bytesRead = stream.read(buffer)
// Fill the whole block. InputStream.read may return a short count mid-file, but the
// block protocol treats any block shorter than BLOCK_SIZE as the final one (EOF), so
// a short mid-file read would truncate the file to silence. Loop until full or EOF.
var bytesRead = 0
while (bytesRead < BLOCK_SIZE) {
val n = stream.read(buffer, bytesRead, BLOCK_SIZE - bytesRead)
if (n <= 0) break
bytesRead += n
}
if (bytesRead <= 0) {
// End of file
@@ -171,11 +179,13 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
blockSendBuffer = messageComposeBuffer.toByteArray()
}
val sendSize = if (blockSendBuffer.size - (blockSendCount * BLOCK_SIZE) < BLOCK_SIZE)
blockSendBuffer.size % BLOCK_SIZE
else if (blockSendBuffer.size <= BLOCK_SIZE)
blockSendBuffer.size
else BLOCK_SIZE
// Bytes still unsent in this block. The old `size % BLOCK_SIZE` was wrong once the buffer
// was already consumed (blockSendCount past the end): for a non-BLOCK_SIZE-multiple message
// it returned the leftover count and then indexed blockSendBuffer[blockSendCount*BLOCK_SIZE+it]
// — e.g. a 6-byte message read one block too far → "Index 4096 out of bounds for length 6".
// Clamp to [0, BLOCK_SIZE]; 0 sends an empty terminating block (same EOF signal as streaming).
val remaining = blockSendBuffer.size - blockSendCount * BLOCK_SIZE
val sendSize = remaining.coerceIn(0, BLOCK_SIZE)
// println("blockSendCount = ${blockSendCount}; sendSize = $sendSize; blockSendBuffer.size = ${blockSendBuffer.size}")

View File

@@ -115,11 +115,13 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, theTevdPath:
blockSendBuffer = messageComposeBuffer.toByteArray()
}
val sendSize = if (blockSendBuffer.size - (blockSendCount * BLOCK_SIZE) < BLOCK_SIZE)
blockSendBuffer.size % BLOCK_SIZE
else if (blockSendBuffer.size <= BLOCK_SIZE)
blockSendBuffer.size
else BLOCK_SIZE
// Bytes still unsent in this block. The old `size % BLOCK_SIZE` was wrong once the buffer
// was already consumed (blockSendCount past the end): for a non-BLOCK_SIZE-multiple message
// it returned the leftover count and then indexed blockSendBuffer[blockSendCount*BLOCK_SIZE+it]
// — e.g. a 6-byte message read one block too far → "Index 4096 out of bounds for length 6".
// Clamp to [0, BLOCK_SIZE]; 0 sends an empty terminating block (same EOF signal as streaming).
val remaining = blockSendBuffer.size - blockSendCount * BLOCK_SIZE
val sendSize = remaining.coerceIn(0, BLOCK_SIZE)
// println("blockSendCount = ${blockSendCount}; sendSize = $sendSize; blockSendBuffer.size = ${blockSendBuffer.size}")