From 646af9452ccc74ce87cf09aad576575acd88474e Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 20 Jun 2026 17:27:27 +0900 Subject: [PATCH] wav direct upload with bugfixes --- assets/disk0/tvdos/bin/playmp2.js | 24 ++++++- assets/disk0/tvdos/bin/playpcm.js | 54 +++++++++------- assets/disk0/tvdos/bin/playtad.js | 24 ++++++- assets/disk0/tvdos/bin/playwav.js | 63 ++++++++++++------- assets/disk0/tvdos/include/playgui.mjs | 12 +++- assets/disk0/tvdos/include/seqread.mjs | 34 ++++++++-- terranmon.txt | 4 +- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 22 +++++++ .../torvald/tsvm/peripheral/AudioAdapter.kt | 25 +++++++- .../torvald/tsvm/peripheral/TestDiskDrive.kt | 22 +++++-- .../torvald/tsvm/peripheral/TevdDiskDrive.kt | 12 ++-- 11 files changed, 228 insertions(+), 68 deletions(-) diff --git a/assets/disk0/tvdos/bin/playmp2.js b/assets/disk0/tvdos/bin/playmp2.js index b2868f9..1023118 100644 --- a/assets/disk0/tvdos/bin/playmp2.js +++ b/assets/disk0/tvdos/bin/playmp2.js @@ -28,6 +28,10 @@ class SequentialFileBuffer { get fileHeader() { return this.seq.fileHeader } } +// Load the visualiser's font ROM now, while no audio file is streaming. The drive is +// single-file-open, so loading it lazily during playback would corrupt the audio stream. +if (gui) gui.preloadAssets() + const filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full) const FILE_SIZE = filebuf.length const FRAME_SIZE = audio.mp2GetInitialFrameSize(filebuf.fileHeader) @@ -82,7 +86,12 @@ let stopPlay = false let errorlevel = 0 try { while (bytes_left > 0 && !stopPlay) { - if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break } + if (interactive && gui.audioIsExitRequested()) { + // Stop immediately and drop everything still queued, so audio doesn't keep playing + // the buffered chunks after the user quits. + audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD) + stopPlay = true; break + } filebuf.readBytes(FRAME_SIZE, SND_BASE_ADDR - 2368) audio.mp2Decode() @@ -101,7 +110,7 @@ try { sys.sleep(bufRealTimeLen) } } - audio.mp2UploadDecoded(0) + audio.mp2UploadDecoded(PLAYHEAD) if (interactive) { gui.audioSetProgress(decodedLength / FILE_SIZE, @@ -115,8 +124,19 @@ try { } } catch (e) { printerrln(e) + // Recover + show the host (Java) stack trace, which `e` alone does not carry. + try { printerrln(sys.printStackTrace(e)) } catch (_) {} errorlevel = 1 } finally { + // Never leave the playhead in 'play' mode for the next program. On a clean finish, let the + // queued tail play out first; on Backspace/error, stop immediately. + if (!stopPlay && errorlevel === 0) { + let guard = 0 + while (audio.getPosition(PLAYHEAD) > 0 && guard++ < 1500) sys.sleep(20) // drain, capped ~30s + } + audio.stop(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) + if (interactive) { if (mp2VisScratch) sys.free(mp2VisScratch) gui.audioClose() diff --git a/assets/disk0/tvdos/bin/playpcm.js b/assets/disk0/tvdos/bin/playpcm.js index 23782ac..77e1b51 100644 --- a/assets/disk0/tvdos/bin/playpcm.js +++ b/assets/disk0/tvdos/bin/playpcm.js @@ -20,6 +20,9 @@ const byterate = 2 * samplingRate function bytesToSec(i) { return i / byterate } +// Load the visualiser's font ROM now, while no audio file is streaming (single-file-open drive). +if (gui) gui.preloadAssets() + seqread.prepare(filePath) const readPtr = sys.malloc(BLOCK_SIZE) @@ -44,29 +47,29 @@ let errorlevel = 0 let readLength = 1 try { while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) { - if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break } - - const queueSize = audio.getPosition(PLAYHEAD) - if (queueSize <= 1) { - for (let repeat = QUEUE_MAX - queueSize; repeat > 0; repeat--) { - const remainingBytes = FILE_SIZE - seqread.getReadCount() - readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE - if (readLength <= 0) break - - seqread.readBytes(readLength, readPtr) - - // Raw PCMu8 stereo — sampleCount = bytes / 2. - if (interactive) gui.audioFeedPcm(readPtr, readLength >> 1) - - audio.putPcmDataByPtr(PLAYHEAD, readPtr, readLength, 0) - audio.setSampleUploadLength(PLAYHEAD, readLength) - audio.startSampleUpload(PLAYHEAD) - - if (repeat > 1) sys.sleep(10) - } - audio.play(PLAYHEAD) + if (interactive && gui.audioIsExitRequested()) { + // Stop immediately and drop everything still queued, so audio doesn't keep playing + // the buffered chunks after the user quits. + audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD) + stopPlay = true; break } + // Top the queue up to QUEUE_MAX chunks with a DIRECT enqueue (no putPcmData/startUpload + // handshake, no sys.sleep). The handshake dropped chunks under load → skips/fast-forward. + while (audio.getPosition(PLAYHEAD) < QUEUE_MAX && seqread.getReadCount() < FILE_SIZE) { + const remainingBytes = FILE_SIZE - seqread.getReadCount() + readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE + if (readLength <= 0) break + + seqread.readBytes(readLength, readPtr) + + // Raw PCMu8 stereo — sampleCount = bytes / 2. + if (interactive) gui.audioFeedPcm(readPtr, readLength >> 1) + + audio.queuePcmDataByPtr(PLAYHEAD, readPtr, readLength) + } + audio.play(PLAYHEAD) + if (interactive) { const cur = seqread.getReadCount() gui.audioSetProgress(cur / FILE_SIZE, bytesToSec(cur), bytesToSec(FILE_SIZE)) @@ -78,6 +81,15 @@ try { printerrln(e) errorlevel = 1 } finally { + // Never leave the playhead in 'play' mode for the next program. On a clean finish, let the + // queued tail play out first; on Backspace/error, stop immediately. + if (!stopPlay && errorlevel === 0) { + let guard = 0 + while (audio.getPosition(PLAYHEAD) > 0 && guard++ < 1500) sys.sleep(20) // drain, capped ~30s + } + audio.stop(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) + if (readPtr !== undefined) sys.free(readPtr) if (interactive) gui.audioClose() } diff --git a/assets/disk0/tvdos/bin/playtad.js b/assets/disk0/tvdos/bin/playtad.js index 47d02f9..4f39d58 100644 --- a/assets/disk0/tvdos/bin/playtad.js +++ b/assets/disk0/tvdos/bin/playtad.js @@ -60,6 +60,9 @@ class SequentialFileBuffer { getReadCount() { return this.seq.getReadCount() } } +// Load the visualiser's font ROM now, while no audio file is streaming (single-file-open drive). +if (gui) gui.preloadAssets() + const filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full) const FILE_SIZE = filebuf.length @@ -132,7 +135,12 @@ let stopPlay = false let errorlevel = 0 try { while (bytes_left > 0 && !stopPlay) { - if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break } + if (interactive && gui.audioIsExitRequested()) { + // Stop immediately and drop everything still queued, so audio doesn't keep playing + // the buffered chunks after the user quits. + audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD) + stopPlay = true; break + } const sampleCount = filebuf.readShort() const maxIndex = filebuf.readByte() @@ -184,7 +192,10 @@ try { bytesToSec(decodedLength), bytesToSec(FILE_SIZE)) let sliceOff = 0 while (sliceOff < sampleCount && !stopPlay) { - if (gui.audioIsExitRequested()) { stopPlay = true; break } + if (gui.audioIsExitRequested()) { + audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD) + stopPlay = true; break + } const sliceN = Math.min(TAD_VIS_SLICE, sampleCount - sliceOff) // tadDecodedBin is negative-addressed: sample i sits at // TAD_DECODED_ADDR - i*2. audioFeedPcm flips the read @@ -212,6 +223,15 @@ try { printerrln(e) errorlevel = 1 } finally { + // Never leave the playhead in 'play' mode for the next program. On a clean finish, let the + // queued tail play out first; on Backspace/error, stop immediately. + if (!stopPlay && errorlevel === 0) { + let guard = 0 + while (audio.getPosition(PLAYHEAD) > 0 && guard++ < 1500) sys.sleep(20) // drain, capped ~30s + } + audio.stop(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) + if (interactive) gui.audioClose() } diff --git a/assets/disk0/tvdos/bin/playwav.js b/assets/disk0/tvdos/bin/playwav.js index 85710b0..377d321 100644 --- a/assets/disk0/tvdos/bin/playwav.js +++ b/assets/disk0/tvdos/bin/playwav.js @@ -26,6 +26,10 @@ function GCD(a, b) { } function LCM(a, b) { return (!a || !b) ? 0 : Math.abs((a * b) / GCD(a, b)) } +// Load the visualiser's font ROM now, while no audio file is streaming. The drive is +// single-file-open, so loading it lazily during playback would corrupt the audio stream. +if (gui) gui.preloadAssets() + seqread.prepare(filePath) if (seqread.readFourCC() !== "RIFF") throw Error("File not RIFF") const FILE_SIZE = seqread.readInt() @@ -142,30 +146,34 @@ try { let readLength = 1 while (!stopPlay && seqread.getReadCount() < startOffset + chunkSize && readLength > 0) { - if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break } - - if (audio.getPosition(PLAYHEAD) <= 1) { - for (let repeat = 0; repeat < QUEUE_MAX; repeat++) { - const remainingBytes = FILE_SIZE - 8 - seqread.getReadCount() - readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE - if (readLength <= 0) break - - seqread.readBytes(readLength, readPtr) - const decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength) - - // Hand the decoded PCMu8 stereo block to the visualiser - // before queueing — the buffer is reused next iteration. - if (interactive) gui.audioFeedPcm(decodePtr, decodedSampleLength >> 1) - - audio.putPcmDataByPtr(PLAYHEAD, decodePtr, decodedSampleLength, 0) - audio.setSampleUploadLength(PLAYHEAD, decodedSampleLength) - audio.startSampleUpload(PLAYHEAD) - - sys.spin() - } - audio.play(PLAYHEAD) + if (interactive && gui.audioIsExitRequested()) { + // Stop immediately and drop everything still queued, so audio doesn't keep + // playing the ~half-second of buffered chunks after the user quits. + audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD) + stopPlay = true; break } + // Top the queue up to QUEUE_MAX chunks with a DIRECT enqueue (no putPcmData/ + // setSampleUploadLength/startSampleUpload handshake, no sys.spin). The handshake + // routes through a single-slot pcmBin and dropped chunks when fed in a burst, which + // skipped/fast-forwarded the song. queuePcmDataByPtr enqueues synchronously. + while (audio.getPosition(PLAYHEAD) < QUEUE_MAX && + seqread.getReadCount() < startOffset + chunkSize) { + const remainingBytes = FILE_SIZE - 8 - seqread.getReadCount() + readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE + if (readLength <= 0) break + + seqread.readBytes(readLength, readPtr) + const decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength) + + // Hand the decoded PCMu8 stereo block to the visualiser before queueing — + // the buffer is reused next iteration. + if (interactive) gui.audioFeedPcm(decodePtr, decodedSampleLength >> 1) + + audio.queuePcmDataByPtr(PLAYHEAD, decodePtr, decodedSampleLength) + } + audio.play(PLAYHEAD) + if (interactive) { const cur = seqread.getReadCount() - startOffset const tot = FILE_SIZE - startOffset - 8 @@ -174,6 +182,15 @@ try { } sys.sleep(10) } + + // Release the playhead so it isn't left in 'play' mode for the next program. On a clean + // finish, let the queued tail play out first; on Backspace/error, stop immediately. + if (!stopPlay && errorlevel === 0) { + let guard = 0 + while (audio.getPosition(PLAYHEAD) > 0 && guard++ < 1500) sys.sleep(20) // drain, ~30s cap + } + audio.stop(PLAYHEAD) + audio.purgeQueue(PLAYHEAD) } else { seqread.skip(chunkSize) @@ -183,6 +200,8 @@ try { } } catch (e) { printerrln(e) + // Recover + show the host (Java) stack trace, which `e` alone does not carry. + try { printerrln(sys.printStackTrace(e)) } catch (_) {} errorlevel = 1 } finally { if (readPtr !== undefined) sys.free(readPtr) diff --git a/assets/disk0/tvdos/include/playgui.mjs b/assets/disk0/tvdos/include/playgui.mjs index 158ee3b..4ebb56d 100644 --- a/assets/disk0/tvdos/include/playgui.mjs +++ b/assets/disk0/tvdos/include/playgui.mjs @@ -1238,6 +1238,15 @@ function audioSetProgress(progress, elapsedSec, totalSec) { ag_drawProgress(progress, elapsedSec | 0, totalSec | 0) } +// Pre-build the mini-AAlib glyph tables (which loads the 7x14 font ROM from disk). MUST be +// called BEFORE the media file is opened: the wavescope's first render would otherwise +// files.open() the font ROM mid-playback, and a disk drive is single-file-open per drive — +// opening the font while the audio file is being streamed off the same drive corrupts the +// stream (short noise burst, then silence). After this, audioRender does no disk I/O. +function preloadAssets() { + aa_mktable() +} + function audioRender() { const now = sys.nanoTime() if (now - ag_lastRenderNs < AG_RENDER_INTERVAL_NS) return @@ -1274,5 +1283,6 @@ exports = { audioSetProgress, audioRender, audioClose, - audioIsExitRequested + audioIsExitRequested, + preloadAssets } \ No newline at end of file diff --git a/assets/disk0/tvdos/include/seqread.mjs b/assets/disk0/tvdos/include/seqread.mjs index e79db8c..1f6e450 100644 --- a/assets/disk0/tvdos/include/seqread.mjs +++ b/assets/disk0/tvdos/include/seqread.mjs @@ -5,12 +5,17 @@ let readCount = 0 let port = undefined let fileHeader = new Uint8Array(4096) +// Valid byte count of the block currently sitting in the read buffer. The disk's last block is +// usually shorter than 4096; without tracking this the reuse path read 4096-padding bytes of stale +// buffer past EOF (white-noise burst / the old OOB crash). +let curBlockLen = 0 function prepare(fullPath) { if (fullPath[2] != '/' && fullPath[2] != '\\') throw Error("Expected full path with drive letter, got " + fullPath) readCount = 0 + curBlockLen = 0 let driveLetter = fullPath[0].toUpperCase() let diskPath = fullPath.substring(2).replaceAll("\\",'/') @@ -68,12 +73,18 @@ function readBytes(length, ptrToDecode) { let blockTransferStatus = ((sys.peek(-4085 - port*2) & 255) | ((sys.peek(-4086 - port*2) & 255) << 8)) let thisBlockLen = blockTransferStatus & 4095 - if (thisBlockLen == 0) thisBlockLen = 4096 // [1, 4096] - let hasMore = (blockTransferStatus & 0x8000 != 0) + // bit 12 (0x1000) of the status = "the disk's block size is exactly 0" — the EOF marker. + // Without it a 0-length terminating block is indistinguishable from a full 4096-byte + // block (4096 & 4095 == 0 too), so the old code read 4096 bytes of stale buffer past EOF. + let blockIsEmpty = (blockTransferStatus & 0x1000) != 0 + if (thisBlockLen == 0 && !blockIsEmpty) thisBlockLen = 4096 // [1, 4096] + curBlockLen = thisBlockLen // serial.println(`block: (${thisBlockLen})[${[...Array(thisBlockLen).keys()].map(k => (sys.peek(-4097 - k) & 255).toString(16).padStart(2,'0')).join()}]`) + if (thisBlockLen == 0) break // EOF: nothing more to read (zero-filled below) + let remaining = Math.min(thisBlockLen, length - completedReads) // serial.println(`Pulled a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}, remaining = ${remaining}`) @@ -87,11 +98,13 @@ function readBytes(length, ptrToDecode) { } else { let padding = readCount % 4096 - let remaining = length - completedReads - let thisBlockLen = Math.min(4096 - padding, length - completedReads) + // Only `curBlockLen - padding` bytes of the buffered block are real; the rest is stale. + // A short final block leaves padding >= curBlockLen, i.e. we are past EOF. + let avail = curBlockLen - padding + if (avail <= 0) break // past the short last block: EOF (zero-filled below) + let thisBlockLen = Math.min(avail, length - completedReads) -// serial.println(`padding = ${padding}; remaining = ${remaining}`) -// serial.println(`block: (${thisBlockLen})[${[...Array(thisBlockLen).keys()].map(k => (sys.peek(-4097 - padding - k) & 255).toString(16).padStart(2,'0')).join()}]`) +// serial.println(`padding = ${padding}; avail = ${avail}`) // serial.println(`Reusing a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}`) // copy from read buffer to designated position @@ -103,6 +116,15 @@ function readBytes(length, ptrToDecode) { } } + // Reached EOF before satisfying the request: zero-fill the remainder so callers get defined + // bytes (silence, for audio) instead of stale garbage, and advance readCount so the caller's + // read loop still terminates (it was relying on readCount reaching the requested position). + while (completedReads < length) { + sys.poke(ptr + completedReads * destVector, 0) + completedReads += 1 + readCount += 1 + } + //serial.println(`END readBytes(${length}); readCount = ${readCount}\n`) return ptr diff --git a/terranmon.txt b/terranmon.txt index 7d51180..0bc0330 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -122,7 +122,9 @@ MMIO 94..99 RW: Beeper command 0bPPPPPPPP 0bpppppp_QQ 0bqqAABBCC 0baaaaaaaa 0bbbbbbbbb 0bcccccccc - PPPPPPPPpppppp: 14-bit frequency divider (master clock: 3579545 / 16 Hz), determines pitch. + Master clock: 3579545.4545... Hz with prescaler of 16 + + PPPPPPPPpppppp: 14-bit frequency divider, determines pitch. 0: no sound QQ: note effect 00: none diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index e3f4a91..8937ea5 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -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 } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 8578b92..ce2d075 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -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") diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt index 69ddb1b..81ff1d6 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/TestDiskDrive.kt @@ -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}") diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt index 5b90bc2..83cb97e 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/TevdDiskDrive.kt @@ -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}")