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

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

View File

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

View File

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

View File

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

View File

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

View File

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