mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-20 19:24:04 +09:00
wav direct upload with bugfixes
This commit is contained in:
@@ -28,6 +28,10 @@ class SequentialFileBuffer {
|
|||||||
get fileHeader() { return this.seq.fileHeader }
|
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 filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||||
const FILE_SIZE = filebuf.length
|
const FILE_SIZE = filebuf.length
|
||||||
const FRAME_SIZE = audio.mp2GetInitialFrameSize(filebuf.fileHeader)
|
const FRAME_SIZE = audio.mp2GetInitialFrameSize(filebuf.fileHeader)
|
||||||
@@ -82,7 +86,12 @@ let stopPlay = false
|
|||||||
let errorlevel = 0
|
let errorlevel = 0
|
||||||
try {
|
try {
|
||||||
while (bytes_left > 0 && !stopPlay) {
|
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)
|
filebuf.readBytes(FRAME_SIZE, SND_BASE_ADDR - 2368)
|
||||||
audio.mp2Decode()
|
audio.mp2Decode()
|
||||||
@@ -101,7 +110,7 @@ try {
|
|||||||
sys.sleep(bufRealTimeLen)
|
sys.sleep(bufRealTimeLen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audio.mp2UploadDecoded(0)
|
audio.mp2UploadDecoded(PLAYHEAD)
|
||||||
|
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
gui.audioSetProgress(decodedLength / FILE_SIZE,
|
gui.audioSetProgress(decodedLength / FILE_SIZE,
|
||||||
@@ -115,8 +124,19 @@ try {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printerrln(e)
|
printerrln(e)
|
||||||
|
// Recover + show the host (Java) stack trace, which `e` alone does not carry.
|
||||||
|
try { printerrln(sys.printStackTrace(e)) } catch (_) {}
|
||||||
errorlevel = 1
|
errorlevel = 1
|
||||||
} finally {
|
} 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 (interactive) {
|
||||||
if (mp2VisScratch) sys.free(mp2VisScratch)
|
if (mp2VisScratch) sys.free(mp2VisScratch)
|
||||||
gui.audioClose()
|
gui.audioClose()
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const byterate = 2 * samplingRate
|
|||||||
|
|
||||||
function bytesToSec(i) { return i / byterate }
|
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)
|
seqread.prepare(filePath)
|
||||||
|
|
||||||
const readPtr = sys.malloc(BLOCK_SIZE)
|
const readPtr = sys.malloc(BLOCK_SIZE)
|
||||||
@@ -44,29 +47,29 @@ let errorlevel = 0
|
|||||||
let readLength = 1
|
let readLength = 1
|
||||||
try {
|
try {
|
||||||
while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
|
while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
|
||||||
if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break }
|
if (interactive && gui.audioIsExitRequested()) {
|
||||||
|
// Stop immediately and drop everything still queued, so audio doesn't keep playing
|
||||||
const queueSize = audio.getPosition(PLAYHEAD)
|
// the buffered chunks after the user quits.
|
||||||
if (queueSize <= 1) {
|
audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD)
|
||||||
for (let repeat = QUEUE_MAX - queueSize; repeat > 0; repeat--) {
|
stopPlay = true; break
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (interactive) {
|
||||||
const cur = seqread.getReadCount()
|
const cur = seqread.getReadCount()
|
||||||
gui.audioSetProgress(cur / FILE_SIZE, bytesToSec(cur), bytesToSec(FILE_SIZE))
|
gui.audioSetProgress(cur / FILE_SIZE, bytesToSec(cur), bytesToSec(FILE_SIZE))
|
||||||
@@ -78,6 +81,15 @@ try {
|
|||||||
printerrln(e)
|
printerrln(e)
|
||||||
errorlevel = 1
|
errorlevel = 1
|
||||||
} finally {
|
} 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 (readPtr !== undefined) sys.free(readPtr)
|
||||||
if (interactive) gui.audioClose()
|
if (interactive) gui.audioClose()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ class SequentialFileBuffer {
|
|||||||
getReadCount() { return this.seq.getReadCount() }
|
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 filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||||
const FILE_SIZE = filebuf.length
|
const FILE_SIZE = filebuf.length
|
||||||
|
|
||||||
@@ -132,7 +135,12 @@ let stopPlay = false
|
|||||||
let errorlevel = 0
|
let errorlevel = 0
|
||||||
try {
|
try {
|
||||||
while (bytes_left > 0 && !stopPlay) {
|
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 sampleCount = filebuf.readShort()
|
||||||
const maxIndex = filebuf.readByte()
|
const maxIndex = filebuf.readByte()
|
||||||
@@ -184,7 +192,10 @@ try {
|
|||||||
bytesToSec(decodedLength), bytesToSec(FILE_SIZE))
|
bytesToSec(decodedLength), bytesToSec(FILE_SIZE))
|
||||||
let sliceOff = 0
|
let sliceOff = 0
|
||||||
while (sliceOff < sampleCount && !stopPlay) {
|
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)
|
const sliceN = Math.min(TAD_VIS_SLICE, sampleCount - sliceOff)
|
||||||
// tadDecodedBin is negative-addressed: sample i sits at
|
// tadDecodedBin is negative-addressed: sample i sits at
|
||||||
// TAD_DECODED_ADDR - i*2. audioFeedPcm flips the read
|
// TAD_DECODED_ADDR - i*2. audioFeedPcm flips the read
|
||||||
@@ -212,6 +223,15 @@ try {
|
|||||||
printerrln(e)
|
printerrln(e)
|
||||||
errorlevel = 1
|
errorlevel = 1
|
||||||
} finally {
|
} 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()
|
if (interactive) gui.audioClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ function GCD(a, b) {
|
|||||||
}
|
}
|
||||||
function LCM(a, b) { return (!a || !b) ? 0 : Math.abs((a * b) / 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)
|
seqread.prepare(filePath)
|
||||||
if (seqread.readFourCC() !== "RIFF") throw Error("File not RIFF")
|
if (seqread.readFourCC() !== "RIFF") throw Error("File not RIFF")
|
||||||
const FILE_SIZE = seqread.readInt()
|
const FILE_SIZE = seqread.readInt()
|
||||||
@@ -142,30 +146,34 @@ try {
|
|||||||
|
|
||||||
let readLength = 1
|
let readLength = 1
|
||||||
while (!stopPlay && seqread.getReadCount() < startOffset + chunkSize && readLength > 0) {
|
while (!stopPlay && seqread.getReadCount() < startOffset + chunkSize && readLength > 0) {
|
||||||
if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break }
|
if (interactive && gui.audioIsExitRequested()) {
|
||||||
|
// Stop immediately and drop everything still queued, so audio doesn't keep
|
||||||
if (audio.getPosition(PLAYHEAD) <= 1) {
|
// playing the ~half-second of buffered chunks after the user quits.
|
||||||
for (let repeat = 0; repeat < QUEUE_MAX; repeat++) {
|
audio.stop(PLAYHEAD); audio.purgeQueue(PLAYHEAD)
|
||||||
const remainingBytes = FILE_SIZE - 8 - seqread.getReadCount()
|
stopPlay = true; break
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (interactive) {
|
||||||
const cur = seqread.getReadCount() - startOffset
|
const cur = seqread.getReadCount() - startOffset
|
||||||
const tot = FILE_SIZE - startOffset - 8
|
const tot = FILE_SIZE - startOffset - 8
|
||||||
@@ -174,6 +182,15 @@ try {
|
|||||||
}
|
}
|
||||||
sys.sleep(10)
|
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 {
|
else {
|
||||||
seqread.skip(chunkSize)
|
seqread.skip(chunkSize)
|
||||||
@@ -183,6 +200,8 @@ try {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printerrln(e)
|
printerrln(e)
|
||||||
|
// Recover + show the host (Java) stack trace, which `e` alone does not carry.
|
||||||
|
try { printerrln(sys.printStackTrace(e)) } catch (_) {}
|
||||||
errorlevel = 1
|
errorlevel = 1
|
||||||
} finally {
|
} finally {
|
||||||
if (readPtr !== undefined) sys.free(readPtr)
|
if (readPtr !== undefined) sys.free(readPtr)
|
||||||
|
|||||||
@@ -1238,6 +1238,15 @@ function audioSetProgress(progress, elapsedSec, totalSec) {
|
|||||||
ag_drawProgress(progress, elapsedSec | 0, totalSec | 0)
|
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() {
|
function audioRender() {
|
||||||
const now = sys.nanoTime()
|
const now = sys.nanoTime()
|
||||||
if (now - ag_lastRenderNs < AG_RENDER_INTERVAL_NS) return
|
if (now - ag_lastRenderNs < AG_RENDER_INTERVAL_NS) return
|
||||||
@@ -1274,5 +1283,6 @@ exports = {
|
|||||||
audioSetProgress,
|
audioSetProgress,
|
||||||
audioRender,
|
audioRender,
|
||||||
audioClose,
|
audioClose,
|
||||||
audioIsExitRequested
|
audioIsExitRequested,
|
||||||
|
preloadAssets
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
let readCount = 0
|
let readCount = 0
|
||||||
let port = undefined
|
let port = undefined
|
||||||
let fileHeader = new Uint8Array(4096)
|
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) {
|
function prepare(fullPath) {
|
||||||
if (fullPath[2] != '/' && fullPath[2] != '\\') throw Error("Expected full path with drive letter, got " + fullPath)
|
if (fullPath[2] != '/' && fullPath[2] != '\\') throw Error("Expected full path with drive letter, got " + fullPath)
|
||||||
|
|
||||||
|
|
||||||
readCount = 0
|
readCount = 0
|
||||||
|
curBlockLen = 0
|
||||||
|
|
||||||
let driveLetter = fullPath[0].toUpperCase()
|
let driveLetter = fullPath[0].toUpperCase()
|
||||||
let diskPath = fullPath.substring(2).replaceAll("\\",'/')
|
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 blockTransferStatus = ((sys.peek(-4085 - port*2) & 255) | ((sys.peek(-4086 - port*2) & 255) << 8))
|
||||||
let thisBlockLen = blockTransferStatus & 4095
|
let thisBlockLen = blockTransferStatus & 4095
|
||||||
if (thisBlockLen == 0) thisBlockLen = 4096 // [1, 4096]
|
// bit 12 (0x1000) of the status = "the disk's block size is exactly 0" — the EOF marker.
|
||||||
let hasMore = (blockTransferStatus & 0x8000 != 0)
|
// 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()}]`)
|
// 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)
|
let remaining = Math.min(thisBlockLen, length - completedReads)
|
||||||
|
|
||||||
// serial.println(`Pulled a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}, remaining = ${remaining}`)
|
// serial.println(`Pulled a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}, remaining = ${remaining}`)
|
||||||
@@ -87,11 +98,13 @@ function readBytes(length, ptrToDecode) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let padding = readCount % 4096
|
let padding = readCount % 4096
|
||||||
let remaining = length - completedReads
|
// Only `curBlockLen - padding` bytes of the buffered block are real; the rest is stale.
|
||||||
let thisBlockLen = Math.min(4096 - padding, length - completedReads)
|
// 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(`padding = ${padding}; avail = ${avail}`)
|
||||||
// serial.println(`block: (${thisBlockLen})[${[...Array(thisBlockLen).keys()].map(k => (sys.peek(-4097 - padding - k) & 255).toString(16).padStart(2,'0')).join()}]`)
|
|
||||||
// serial.println(`Reusing a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}`)
|
// serial.println(`Reusing a block (${thisBlockLen}); readCount = ${readCount}, completedReads = ${completedReads}`)
|
||||||
|
|
||||||
// copy from read buffer to designated position
|
// 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`)
|
//serial.println(`END readBytes(${length}); readCount = ${readCount}\n`)
|
||||||
|
|
||||||
return ptr
|
return ptr
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ MMIO
|
|||||||
94..99 RW: Beeper command
|
94..99 RW: Beeper command
|
||||||
0bPPPPPPPP 0bpppppp_QQ 0bqqAABBCC 0baaaaaaaa 0bbbbbbbbb 0bcccccccc
|
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
|
0: no sound
|
||||||
QQ: note effect
|
QQ: note effect
|
||||||
00: none
|
00: none
|
||||||
|
|||||||
@@ -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 getPcmData(playhead: Int, index: Int) = getFirstSnd()?.pcmBin?.get(playhead)?.get(index.toLong())
|
||||||
|
|
||||||
fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index }
|
fun setPcmQueueCapacityIndex(playhead: Int, index: Int) { getPlayhead(playhead)?.pcmQueueSizeIndex = index }
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
|||||||
private fun printdbg(msg: Any) {
|
private fun printdbg(msg: Any) {
|
||||||
if (AudioAdapter.DBGPRN) println("[AudioAdapter] $msg")
|
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() {
|
override fun run() {
|
||||||
while (!Thread.currentThread().isInterrupted) {
|
while (!Thread.currentThread().isInterrupted) {
|
||||||
try {
|
try {
|
||||||
@@ -43,7 +48,19 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
|||||||
|
|
||||||
playhead.position = writeQueue.size
|
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)
|
Thread.sleep(6)
|
||||||
}
|
}
|
||||||
@@ -129,6 +146,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal val DBGPRN = false
|
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 SAMPLING_RATE = 32000
|
||||||
const val TRACKER_CHUNK = 512
|
const val TRACKER_CHUNK = 512
|
||||||
// Per-voice soundscope ring-buffer length. Power of two so wrap-around is a single AND.
|
// 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("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...")
|
||||||
// printdbg(""+(0..42).joinToString { String.format("%.2f", samples[it]) })
|
// 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")
|
// printdbg("P${playhead.index+1} go back to spinning")
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,15 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val buffer = ByteArray(BLOCK_SIZE)
|
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) {
|
if (bytesRead <= 0) {
|
||||||
// End of file
|
// End of file
|
||||||
@@ -171,11 +179,13 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
|
|||||||
blockSendBuffer = messageComposeBuffer.toByteArray()
|
blockSendBuffer = messageComposeBuffer.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sendSize = if (blockSendBuffer.size - (blockSendCount * BLOCK_SIZE) < BLOCK_SIZE)
|
// Bytes still unsent in this block. The old `size % BLOCK_SIZE` was wrong once the buffer
|
||||||
blockSendBuffer.size % BLOCK_SIZE
|
// was already consumed (blockSendCount past the end): for a non-BLOCK_SIZE-multiple message
|
||||||
else if (blockSendBuffer.size <= BLOCK_SIZE)
|
// it returned the leftover count and then indexed blockSendBuffer[blockSendCount*BLOCK_SIZE+it]
|
||||||
blockSendBuffer.size
|
// — e.g. a 6-byte message read one block too far → "Index 4096 out of bounds for length 6".
|
||||||
else BLOCK_SIZE
|
// 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}")
|
// println("blockSendCount = ${blockSendCount}; sendSize = $sendSize; blockSendBuffer.size = ${blockSendBuffer.size}")
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,13 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, theTevdPath:
|
|||||||
blockSendBuffer = messageComposeBuffer.toByteArray()
|
blockSendBuffer = messageComposeBuffer.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sendSize = if (blockSendBuffer.size - (blockSendCount * BLOCK_SIZE) < BLOCK_SIZE)
|
// Bytes still unsent in this block. The old `size % BLOCK_SIZE` was wrong once the buffer
|
||||||
blockSendBuffer.size % BLOCK_SIZE
|
// was already consumed (blockSendCount past the end): for a non-BLOCK_SIZE-multiple message
|
||||||
else if (blockSendBuffer.size <= BLOCK_SIZE)
|
// it returned the leftover count and then indexed blockSendBuffer[blockSendCount*BLOCK_SIZE+it]
|
||||||
blockSendBuffer.size
|
// — e.g. a 6-byte message read one block too far → "Index 4096 out of bounds for length 6".
|
||||||
else BLOCK_SIZE
|
// 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}")
|
// println("blockSendCount = ${blockSendCount}; sendSize = $sendSize; blockSendBuffer.size = ${blockSendBuffer.size}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user