mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-09 14:44:05 +09:00
new visualiser for pcm
This commit is contained in:
@@ -1,209 +1,122 @@
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
// playmp2 — MPEG-1/2 Audio Layer II player with the shared playgui visualiser.
|
||||
// Usage: playmp2 <file.mp2> [-i]
|
||||
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
if (!SND_BASE_ADDR) return 10
|
||||
|
||||
const MP2_BITRATES = ["???", 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384]
|
||||
const MP2_BITRATES = ["???", 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384]
|
||||
const MP2_CHANNELMODES = ["Stereo", "Joint", "Dual", "Mono"]
|
||||
|
||||
const pcm = require("pcm")
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() === "-i"
|
||||
const gui = interactive ? require("playgui") : null
|
||||
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
|
||||
|
||||
class SequentialFileBuffer {
|
||||
|
||||
constructor(path, offset, length) {
|
||||
if (Array.isArray(path)) throw Error("arg #1 is path(string), not array")
|
||||
|
||||
this.path = path
|
||||
this.file = files.open(path)
|
||||
|
||||
this.offset = offset || 0
|
||||
this.originalOffset = offset
|
||||
this.length = length || this.file.size
|
||||
|
||||
this.seq = require("seqread")
|
||||
this.seq.prepare(path)
|
||||
}
|
||||
|
||||
readBytes(size, ptr) {
|
||||
return this.seq.readBytes(size, ptr)
|
||||
}
|
||||
|
||||
readStr(n) {
|
||||
let ptr = this.seq.readBytes(n)
|
||||
let s = ''
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (i >= this.length) break
|
||||
s += String.fromCharCode(sys.peek(ptr + i))
|
||||
}
|
||||
sys.free(ptr)
|
||||
return s
|
||||
}
|
||||
|
||||
unread(diff) {
|
||||
let newSkipLen = this.seq.getReadCount() - diff
|
||||
this.seq.prepare(this.path)
|
||||
this.seq.skip(newSkipLen)
|
||||
}
|
||||
|
||||
rewind() {
|
||||
this.seq.prepare(this.path)
|
||||
}
|
||||
|
||||
seek(p) {
|
||||
this.seq.prepare(this.path)
|
||||
this.seq.skip(p)
|
||||
}
|
||||
|
||||
get byteLength() {
|
||||
return this.length
|
||||
}
|
||||
|
||||
get fileHeader() {
|
||||
return this.seq.fileHeader
|
||||
}
|
||||
|
||||
/*get remaining() {
|
||||
return this.length - this.getReadCount()
|
||||
}*/
|
||||
readBytes(size, ptr) { return this.seq.readBytes(size, ptr) }
|
||||
get fileHeader() { return this.seq.fileHeader }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
const FILE_SIZE = filebuf.length// - 100
|
||||
const FRAME_SIZE = audio.mp2GetInitialFrameSize(filebuf.fileHeader)
|
||||
const filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
const FILE_SIZE = filebuf.length
|
||||
const FRAME_SIZE = audio.mp2GetInitialFrameSize(filebuf.fileHeader)
|
||||
const MEDIA_BITRATE = MP2_BITRATES[filebuf.fileHeader[2] >>> 4]
|
||||
const MEDIA_CHANNEL_MODE = MP2_CHANNELMODES[filebuf.fileHeader[3] >>> 6]
|
||||
const MEDIA_CHANNEL = MP2_CHANNELMODES[filebuf.fileHeader[3] >>> 6]
|
||||
|
||||
// mediaDecodedBin sits at MMIO offset 64 in the audio peripheral and holds
|
||||
// 2304 bytes (1152 stereo u8 samples per MP2 frame). Peripheral memory grows
|
||||
// toward 0 so the canonical pointer is SND_BASE_ADDR - 64.
|
||||
//
|
||||
// IMPORTANT: single-byte sys.peek on this address hits AudioAdapter.peek()
|
||||
// which maps the lower offsets to sampleBin, not mediaDecodedBin (the
|
||||
// MMIO/Memory-Space split — see CLAUDE.md). To get the decoded PCM into the
|
||||
// visualiser, we sys.memcpy mediaDecodedBin → a RAM scratch buffer; memcpy
|
||||
// uses VM.getDev internally which DOES route the MMIO read correctly.
|
||||
//
|
||||
// VM.getDev's range check on mediaDecodedBin (relPtrInDev) is half-open and
|
||||
// won't let us copy the full 2304 bytes — we copy 2302 (one stereo sample
|
||||
// short of the frame, invisible at visualiser resolution).
|
||||
const MP2_DECODED_ADDR = SND_BASE_ADDR - 64
|
||||
const MP2_VIS_COPY_BYTES = 2302
|
||||
const MP2_VIS_SAMPLE_COUNT = MP2_VIS_COPY_BYTES >> 1 // 1151
|
||||
const mp2VisScratch = interactive ? sys.malloc(MP2_VIS_COPY_BYTES) : 0
|
||||
|
||||
let bytes_left = FILE_SIZE
|
||||
let bytes_left = FILE_SIZE
|
||||
let decodedLength = 0
|
||||
|
||||
|
||||
//serial.println(`Frame size: ${FRAME_SIZE}`)
|
||||
|
||||
|
||||
con.curs_set(0)
|
||||
let [__, CONSOLE_WIDTH] = con.getmaxyx()
|
||||
if (interactive) {
|
||||
let [cy, cx] = con.getyx()
|
||||
// file name
|
||||
con.mvaddch(cy, 1)
|
||||
con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5)
|
||||
print(filebuf.file.name)
|
||||
con.prnch(0xC6);con.prnch(0xCD)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 26 - filebuf.file.name.length))
|
||||
con.prnch(0xB5)
|
||||
print("Hold Bksp to Exit")
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB)
|
||||
|
||||
// L R pillar
|
||||
con.prnch(0xBA)
|
||||
con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA)
|
||||
|
||||
// media info
|
||||
let mediaInfoStr = `MP2 ${MEDIA_CHANNEL_MODE} ${MEDIA_BITRATE}kbps`
|
||||
con.move(cy+2,1)
|
||||
con.prnch(0xC8)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length))
|
||||
con.prnch(0xB5)
|
||||
print(mediaInfoStr)
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC)
|
||||
|
||||
con.move(cy+1, 2)
|
||||
}
|
||||
let [cy, cx] = con.getyx()
|
||||
let paintWidth = CONSOLE_WIDTH - 20
|
||||
function bytesToSec(i) {
|
||||
// using fixed value: FRAME_SIZE(216) bytes for 36 ms on sampling rate 32000 Hz
|
||||
return i / (FRAME_SIZE * 1000 / bufRealTimeLen)
|
||||
}
|
||||
function secToReadable(n) {
|
||||
let mins = ''+((n/60)|0)
|
||||
let secs = ''+(n % 60)
|
||||
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
|
||||
}
|
||||
function printPlayBar(currently) {
|
||||
if (interactive) {
|
||||
let currently = decodedLength
|
||||
let total = FILE_SIZE
|
||||
|
||||
let currentlySec = Math.round(bytesToSec(currently))
|
||||
let totalSec = Math.round(bytesToSec(total))
|
||||
|
||||
con.move(cy, 3)
|
||||
print(' '.repeat(15))
|
||||
con.move(cy, 3)
|
||||
|
||||
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
|
||||
|
||||
con.move(cy, 17)
|
||||
print(' ')
|
||||
let progressbar = '\x84196u'.repeat(paintWidth + 1)
|
||||
print(progressbar)
|
||||
|
||||
con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const bufRealTimeLen = 36 // one MP2 frame at 32 kHz ≈ 36 ms
|
||||
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setPcmQueueCapacityIndex(0, 2) // queue size is now 8
|
||||
audio.setPcmQueueCapacityIndex(0, 2)
|
||||
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
audio.play(0)
|
||||
|
||||
|
||||
//let mp2context = audio.mp2Init()
|
||||
audio.mp2Init()
|
||||
|
||||
// decode frame
|
||||
let t1 = sys.nanoTime()
|
||||
let bufRealTimeLen = 36
|
||||
function bytesToSec(i) { return i / (FRAME_SIZE * 1000 / bufRealTimeLen) }
|
||||
|
||||
if (interactive) {
|
||||
const tag = "MP2"
|
||||
const title = `${filebuf.file.name} ${MEDIA_CHANNEL} ${MEDIA_BITRATE}kbps`
|
||||
gui.audioInit({ title, tag })
|
||||
}
|
||||
|
||||
let stopPlay = false
|
||||
let errorlevel = 0
|
||||
try {
|
||||
while (bytes_left > 0 && !stopPlay) {
|
||||
|
||||
if (interactive) {
|
||||
sys.poke(-40, 1)
|
||||
if (sys.peek(-41) == 67) {
|
||||
stopPlay = true
|
||||
}
|
||||
}
|
||||
|
||||
printPlayBar()
|
||||
|
||||
if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break }
|
||||
|
||||
filebuf.readBytes(FRAME_SIZE, SND_BASE_ADDR - 2368)
|
||||
audio.mp2Decode()
|
||||
|
||||
// After decode, 1152 PCMu8 stereo samples sit in mediaDecodedBin
|
||||
// (MMIO). Bounce them through RAM so single-byte peek in the
|
||||
// visualiser pipeline can reach them — see MP2_DECODED_ADDR notes.
|
||||
if (interactive) {
|
||||
sys.memcpy(MP2_DECODED_ADDR, mp2VisScratch, MP2_VIS_COPY_BYTES)
|
||||
gui.audioFeedPcm(mp2VisScratch, MP2_VIS_SAMPLE_COUNT)
|
||||
}
|
||||
|
||||
if (audio.getPosition(0) >= QUEUE_MAX) {
|
||||
while (audio.getPosition(0) >= (QUEUE_MAX >>> 1)) {
|
||||
printdbg(`Queue full, waiting until the queue has some space (${audio.getPosition(0)}/${QUEUE_MAX})`)
|
||||
if (interactive) gui.audioRender()
|
||||
sys.sleep(bufRealTimeLen)
|
||||
}
|
||||
}
|
||||
audio.mp2UploadDecoded(0)
|
||||
|
||||
if (interactive) {
|
||||
gui.audioSetProgress(decodedLength / FILE_SIZE,
|
||||
bytesToSec(decodedLength), bytesToSec(FILE_SIZE))
|
||||
gui.audioRender()
|
||||
}
|
||||
sys.sleep(10)
|
||||
|
||||
|
||||
|
||||
bytes_left -= FRAME_SIZE
|
||||
bytes_left -= FRAME_SIZE
|
||||
decodedLength += FRAME_SIZE
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
printerrln(e)
|
||||
errorlevel = 1
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
if (interactive) {
|
||||
if (mp2VisScratch) sys.free(mp2VisScratch)
|
||||
gui.audioClose()
|
||||
}
|
||||
}
|
||||
|
||||
return errorlevel
|
||||
return errorlevel
|
||||
|
||||
@@ -1,196 +1,81 @@
|
||||
// usage: playpcm audiofile.pcm [/i]
|
||||
let fileeeee = files.open(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
let filename = fileeeee.fullPath
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
// playpcm — raw PCMu8 stereo player with the shared playgui visualiser.
|
||||
// Usage: playpcm <file.pcm> [-i]
|
||||
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
|
||||
const pcm = require("pcm")
|
||||
const FILE_SIZE = files.open(filename).size
|
||||
|
||||
|
||||
|
||||
function printComments() {
|
||||
for (const [key, value] of Object.entries(comments)) {
|
||||
printdbg(`${key}: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
function GCD(a, b) {
|
||||
a = Math.abs(a)
|
||||
b = Math.abs(b)
|
||||
if (b > a) {var temp = a; a = b; b = temp}
|
||||
while (true) {
|
||||
if (b == 0) return a
|
||||
a %= b
|
||||
if (a == 0) return b
|
||||
b %= a
|
||||
}
|
||||
}
|
||||
|
||||
function LCM(a, b) {
|
||||
return (!a || !b) ? 0 : Math.abs((a * b) / GCD(a, b))
|
||||
}
|
||||
|
||||
|
||||
|
||||
//println("Reading...")
|
||||
//serial.println("!!! READING")
|
||||
const fileHandle = files.open(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
const filePath = fileHandle.fullPath
|
||||
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() === "-i"
|
||||
const pcm = require("pcm")
|
||||
const seqread = require("seqread")
|
||||
seqread.prepare(filename)
|
||||
|
||||
|
||||
|
||||
|
||||
const gui = interactive ? require("playgui") : null
|
||||
|
||||
const FILE_SIZE = files.open(filePath).size
|
||||
|
||||
let BLOCK_SIZE = 4096
|
||||
let INFILE_BLOCK_SIZE = BLOCK_SIZE
|
||||
const QUEUE_MAX = 8 // according to the spec
|
||||
const INFILE_BLOCK_SIZE = BLOCK_SIZE
|
||||
const QUEUE_MAX = 8
|
||||
|
||||
let nChannels = 2
|
||||
let samplingRate = pcm.HW_SAMPLING_RATE;
|
||||
let blockSize = 2;
|
||||
let bitsPerSample = 8;
|
||||
let byterate = 2*samplingRate;
|
||||
let comments = {};
|
||||
let readPtr = undefined
|
||||
let decodePtr = undefined
|
||||
const samplingRate = pcm.HW_SAMPLING_RATE
|
||||
const byterate = 2 * samplingRate
|
||||
|
||||
function bytesToSec(i) {
|
||||
return i / byterate
|
||||
}
|
||||
function secToReadable(n) {
|
||||
let mins = ''+((n/60)|0)
|
||||
let secs = ''+(n % 60)
|
||||
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
|
||||
}
|
||||
|
||||
let stopPlay = false
|
||||
con.curs_set(0)
|
||||
let [__, CONSOLE_WIDTH] = con.getmaxyx()
|
||||
if (interactive) {
|
||||
let [cy, cx] = con.getyx()
|
||||
// file name
|
||||
con.mvaddch(cy, 1)
|
||||
con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5)
|
||||
print(fileeeee.name)
|
||||
con.prnch(0xC6);con.prnch(0xCD)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 26 - fileeeee.name.length))
|
||||
con.prnch(0xB5)
|
||||
print("Hold Bksp to Exit")
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB)
|
||||
|
||||
// L R pillar
|
||||
con.prnch(0xBA)
|
||||
con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA)
|
||||
|
||||
// media info
|
||||
let mediaInfoStr = `Raw PCM 512kbps`
|
||||
con.move(cy+2,1)
|
||||
con.prnch(0xC8)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length))
|
||||
con.prnch(0xB5)
|
||||
print(mediaInfoStr)
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC)
|
||||
|
||||
con.move(cy+1, 2)
|
||||
}
|
||||
let [cy, cx] = con.getyx()
|
||||
let paintWidth = CONSOLE_WIDTH - 20
|
||||
// read chunks loop
|
||||
readPtr = sys.malloc(BLOCK_SIZE * bitsPerSample / 8)
|
||||
decodePtr = sys.malloc(BLOCK_SIZE * pcm.HW_SAMPLING_RATE / samplingRate)
|
||||
function bytesToSec(i) { return i / byterate }
|
||||
|
||||
seqread.prepare(filePath)
|
||||
|
||||
const readPtr = sys.malloc(BLOCK_SIZE)
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
|
||||
let readLength = 1
|
||||
|
||||
function printPlayBar() {
|
||||
if (interactive) {
|
||||
let currently = seqread.getReadCount()
|
||||
let total = FILE_SIZE
|
||||
|
||||
let currentlySec = Math.round(bytesToSec(currently))
|
||||
let totalSec = Math.round(bytesToSec(total))
|
||||
|
||||
con.move(cy, 3)
|
||||
print(' '.repeat(15))
|
||||
con.move(cy, 3)
|
||||
|
||||
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
|
||||
|
||||
con.move(cy, 17)
|
||||
print(' ')
|
||||
let progressbar = '\x84196u'.repeat(paintWidth + 1)
|
||||
print(progressbar)
|
||||
|
||||
con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB)
|
||||
}
|
||||
if (interactive) {
|
||||
gui.audioInit({
|
||||
title: `${fileHandle.name} Raw PCM 32kHz Stereo`,
|
||||
tag: "PCM"
|
||||
})
|
||||
}
|
||||
|
||||
let stopPlay = false
|
||||
let errorlevel = 0
|
||||
let readLength = 1
|
||||
try {
|
||||
while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
|
||||
if (interactive) {
|
||||
sys.poke(-40, 1)
|
||||
if (sys.peek(-41) == 67) {
|
||||
stopPlay = true
|
||||
}
|
||||
}
|
||||
while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
|
||||
if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break }
|
||||
|
||||
const queueSize = audio.getPosition(0)
|
||||
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
|
||||
|
||||
let queueSize = audio.getPosition(0)
|
||||
if (queueSize <= 1) {
|
||||
seqread.readBytes(readLength, readPtr)
|
||||
|
||||
printPlayBar()
|
||||
// Raw PCMu8 stereo — sampleCount = bytes / 2.
|
||||
if (interactive) gui.audioFeedPcm(readPtr, readLength >> 1)
|
||||
|
||||
// upload four samples for lag-safely
|
||||
for (let repeat = QUEUE_MAX - queueSize; repeat > 0; repeat--) {
|
||||
let remainingBytes = FILE_SIZE - seqread.getReadCount()
|
||||
audio.putPcmDataByPtr(0, readPtr, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE
|
||||
if (readLength <= 0) {
|
||||
printdbg(`readLength = ${readLength}`)
|
||||
break
|
||||
if (repeat > 1) sys.sleep(10)
|
||||
}
|
||||
|
||||
printdbg(`offset: ${seqread.getReadCount()}/${FILE_SIZE}; readLength: ${readLength}`)
|
||||
|
||||
seqread.readBytes(readLength, readPtr)
|
||||
|
||||
audio.putPcmDataByPtr(0, readPtr, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
|
||||
if (repeat > 1) sys.sleep(10)
|
||||
|
||||
printPlayBar()
|
||||
audio.play(0)
|
||||
}
|
||||
|
||||
audio.play(0)
|
||||
if (interactive) {
|
||||
const cur = seqread.getReadCount()
|
||||
gui.audioSetProgress(cur / FILE_SIZE, bytesToSec(cur), bytesToSec(FILE_SIZE))
|
||||
gui.audioRender()
|
||||
}
|
||||
sys.sleep(10)
|
||||
}
|
||||
|
||||
let remainingBytes = FILE_SIZE - seqread.getReadCount()
|
||||
printdbg(`readLength = ${readLength}; remainingBytes2 = ${remainingBytes}; seqread.getReadCount() = ${seqread.getReadCount()};`)
|
||||
|
||||
|
||||
sys.sleep(10)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
printerrln(e)
|
||||
errorlevel = 1
|
||||
}
|
||||
finally {
|
||||
//audio.stop(0)
|
||||
} finally {
|
||||
if (readPtr !== undefined) sys.free(readPtr)
|
||||
if (decodePtr !== undefined) sys.free(decodePtr)
|
||||
if (interactive) gui.audioClose()
|
||||
}
|
||||
|
||||
return errorlevel
|
||||
|
||||
|
||||
@@ -1,114 +1,66 @@
|
||||
// playtad — TAD (TSVM Advanced Audio) player with the shared playgui visualiser.
|
||||
// Usage: playtad <file.tad> [-i | -d]
|
||||
// -i Interactive mode (visualiser + progress bar; hold Backspace to exit)
|
||||
// -d Dump mode (print the first three chunks to serial for debugging)
|
||||
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
const SND_MEM_ADDR = audio.getMemAddr()
|
||||
// tadInputBin lives at audio-local offset 917504 and tadDecodedBin at 983040
|
||||
// (post-bef85f6 memory map; the old 262144 offset now hits the enlarged sampleBin).
|
||||
const TAD_INPUT_ADDR = SND_MEM_ADDR - 917504 // TAD input buffer (matches TAV packet 0x24)
|
||||
const TAD_DECODED_ADDR = SND_MEM_ADDR - 983040 // TAD decoded buffer
|
||||
const SND_MEM_ADDR = audio.getMemAddr()
|
||||
// tadInputBin at offset 917504, tadDecodedBin at 983040. Both addressed via
|
||||
// negative pointers — peripheral memory grows toward 0.
|
||||
const TAD_INPUT_ADDR = SND_MEM_ADDR - 917504
|
||||
const TAD_DECODED_ADDR = SND_MEM_ADDR - 983040
|
||||
|
||||
if (!SND_BASE_ADDR) return 10
|
||||
|
||||
// Check for help flag or missing arguments
|
||||
if (!exec_args[1] || exec_args[1] == "-h" || exec_args[1] == "--help") {
|
||||
serial.println("Usage: playtad <file.tad> [-i | -d] [quality]")
|
||||
serial.println(" -i Interactive mode (progress bar, press Backspace to exit)")
|
||||
serial.println(" -d Dump mode (show first 3 chunks with payload hex and decoded samples)")
|
||||
serial.println("")
|
||||
serial.println("Examples:")
|
||||
serial.println(" playtad audio.tad -i # Play with progress bar")
|
||||
serial.println(" playtad audio.tad -d # Dump first 3 chunks for debugging")
|
||||
if (!exec_args[1] || exec_args[1] === "-h" || exec_args[1] === "--help") {
|
||||
serial.println("Usage: playtad <file.tad> [-i | -d]")
|
||||
serial.println(" -i Interactive mode (visualiser + progress bar)")
|
||||
serial.println(" -d Dump first three chunks for debugging")
|
||||
return 0
|
||||
}
|
||||
|
||||
const pcm = require("pcm")
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
|
||||
const dumpCoeffs = exec_args[2] && exec_args[2].toLowerCase() == "-d"
|
||||
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() === "-i"
|
||||
const dumpCoeffs = exec_args[2] && exec_args[2].toLowerCase() === "-d"
|
||||
const gui = interactive ? require("playgui") : null
|
||||
|
||||
class SequentialFileBuffer {
|
||||
|
||||
constructor(path, offset, length) {
|
||||
constructor(path) {
|
||||
if (Array.isArray(path)) throw Error("arg #1 is path(string), not array")
|
||||
|
||||
this.path = path
|
||||
this.file = files.open(path)
|
||||
|
||||
this.offset = offset || 0
|
||||
this.originalOffset = offset
|
||||
this.length = length || this.file.size
|
||||
|
||||
this.length = this.file.size
|
||||
this.seq = require("seqread")
|
||||
this.seq.prepare(path)
|
||||
}
|
||||
|
||||
readBytes(size, ptr) {
|
||||
return this.seq.readBytes(size, ptr)
|
||||
}
|
||||
|
||||
readBytes(size, ptr) { return this.seq.readBytes(size, ptr) }
|
||||
readByte() {
|
||||
let ptr = this.seq.readBytes(1)
|
||||
let val = sys.peek(ptr)
|
||||
const ptr = this.seq.readBytes(1)
|
||||
const val = sys.peek(ptr)
|
||||
sys.free(ptr)
|
||||
return val
|
||||
}
|
||||
|
||||
readShort() {
|
||||
let ptr = this.seq.readBytes(2)
|
||||
let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8)
|
||||
const ptr = this.seq.readBytes(2)
|
||||
const val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8)
|
||||
sys.free(ptr)
|
||||
return val
|
||||
}
|
||||
|
||||
readInt() {
|
||||
let ptr = this.seq.readBytes(4)
|
||||
let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8) | (sys.peek(ptr + 2) << 16) | (sys.peek(ptr + 3) << 24)
|
||||
const ptr = this.seq.readBytes(4)
|
||||
const val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8) | (sys.peek(ptr + 2) << 16) | (sys.peek(ptr + 3) << 24)
|
||||
sys.free(ptr)
|
||||
return val
|
||||
}
|
||||
|
||||
readStr(n) {
|
||||
let ptr = this.seq.readBytes(n)
|
||||
let s = ''
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (i >= this.length) break
|
||||
s += String.fromCharCode(sys.peek(ptr + i))
|
||||
}
|
||||
sys.free(ptr)
|
||||
return s
|
||||
}
|
||||
|
||||
unread(diff) {
|
||||
let newSkipLen = this.seq.getReadCount() - diff
|
||||
const newSkipLen = this.seq.getReadCount() - diff
|
||||
this.seq.prepare(this.path)
|
||||
this.seq.skip(newSkipLen)
|
||||
}
|
||||
|
||||
rewind() {
|
||||
this.seq.prepare(this.path)
|
||||
}
|
||||
|
||||
seek(p) {
|
||||
this.seq.prepare(this.path)
|
||||
this.seq.skip(p)
|
||||
}
|
||||
|
||||
get byteLength() {
|
||||
return this.length
|
||||
}
|
||||
|
||||
get fileHeader() {
|
||||
return this.seq.fileHeader
|
||||
}
|
||||
|
||||
getReadCount() {
|
||||
return this.seq.getReadCount()
|
||||
}
|
||||
rewind() { this.seq.prepare(this.path) }
|
||||
getReadCount() { return this.seq.getReadCount() }
|
||||
}
|
||||
|
||||
|
||||
// Read TAD chunk header to determine format
|
||||
let 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
|
||||
|
||||
if (FILE_SIZE < 7) {
|
||||
@@ -116,12 +68,12 @@ if (FILE_SIZE < 7) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read first chunk header (standalone TAD format: no TAV wrapper)
|
||||
let firstSampleCount = filebuf.readShort()
|
||||
let firstMaxIndex = filebuf.readByte()
|
||||
let firstPayloadSize = filebuf.readInt()
|
||||
// Peek the first chunk header so we know the chunk size for the rough bytes-
|
||||
// to-seconds conversion shown in the progress bar.
|
||||
const firstSampleCount = filebuf.readShort()
|
||||
const firstMaxIndex = filebuf.readByte()
|
||||
const firstPayloadSize = filebuf.readInt()
|
||||
|
||||
// Validate first chunk
|
||||
if (firstSampleCount < 0 || firstSampleCount > 65536) {
|
||||
serial.println(`ERROR: Invalid sample count ${firstSampleCount}. File may be corrupted.`)
|
||||
return 1
|
||||
@@ -135,148 +87,68 @@ if (firstPayloadSize < 1 || firstPayloadSize > 65536) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Rewind to start
|
||||
filebuf.rewind()
|
||||
|
||||
// Calculate approximate frame info
|
||||
const AVG_CHUNK_SIZE = 7 + firstPayloadSize // TAD header (2+1+4) + payload
|
||||
const SAMPLE_RATE = 32000
|
||||
const bufRealTimeLen = Math.floor((firstSampleCount / SAMPLE_RATE) * 1000) // milliseconds per chunk
|
||||
const AVG_CHUNK_SIZE = 7 + firstPayloadSize
|
||||
const SAMPLE_RATE = 32000
|
||||
const bufRealTimeLen = Math.floor((firstSampleCount / SAMPLE_RATE) * 1000)
|
||||
|
||||
if (dumpCoeffs) {
|
||||
serial.println(`TAD Coefficient Dump Mode`)
|
||||
serial.println(`File: ${filebuf.file.name}`)
|
||||
serial.println(`First chunk header:`)
|
||||
serial.println(` Sample Count: ${firstSampleCount}`)
|
||||
serial.println(` Max Index: ${firstMaxIndex}`)
|
||||
serial.println(` Payload Size: ${firstPayloadSize} bytes`)
|
||||
serial.println(`First chunk: ${firstSampleCount} samples, Q${firstMaxIndex}, ${firstPayloadSize} bytes payload`)
|
||||
serial.println(`Chunk Duration: ${bufRealTimeLen} ms`)
|
||||
serial.println(``)
|
||||
}
|
||||
|
||||
|
||||
let bytes_left = FILE_SIZE
|
||||
let bytes_left = FILE_SIZE
|
||||
let decodedLength = 0
|
||||
let chunkNumber = 0
|
||||
|
||||
|
||||
con.curs_set(0)
|
||||
let [__, CONSOLE_WIDTH] = con.getmaxyx()
|
||||
if (interactive) {
|
||||
let [cy, cx] = con.getyx()
|
||||
// file name
|
||||
con.mvaddch(cy, 1)
|
||||
con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5)
|
||||
print(filebuf.file.name)
|
||||
con.prnch(0xC6);con.prnch(0xCD)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 26 - filebuf.file.name.length))
|
||||
con.prnch(0xB5)
|
||||
print("Hold Bksp to Exit")
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB)
|
||||
|
||||
// L R pillar
|
||||
con.prnch(0xBA)
|
||||
con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA)
|
||||
|
||||
// media info
|
||||
let mediaInfoStr = `TAD Q${firstMaxIndex} ${SAMPLE_RATE/1000}kHz`
|
||||
con.move(cy+2,1)
|
||||
con.prnch(0xC8)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length))
|
||||
con.prnch(0xB5)
|
||||
print(mediaInfoStr)
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC)
|
||||
|
||||
con.move(cy+1, 2)
|
||||
}
|
||||
let [cy, cx] = con.getyx()
|
||||
let paintWidth = CONSOLE_WIDTH - 20
|
||||
let chunkNumber = 0
|
||||
|
||||
function bytesToSec(i) {
|
||||
// Approximate: use first chunk's ratio
|
||||
return Math.round((i / FILE_SIZE) * (FILE_SIZE / AVG_CHUNK_SIZE) * (bufRealTimeLen / 1000))
|
||||
}
|
||||
|
||||
function secToReadable(n) {
|
||||
let mins = ''+((n/60)|0)
|
||||
let secs = ''+(n % 60)
|
||||
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
|
||||
}
|
||||
|
||||
function printPlayBar() {
|
||||
if (interactive) {
|
||||
let currently = decodedLength
|
||||
let total = FILE_SIZE
|
||||
|
||||
let currentlySec = bytesToSec(currently)
|
||||
let totalSec = bytesToSec(total)
|
||||
|
||||
con.move(cy, 3)
|
||||
print(' '.repeat(15))
|
||||
con.move(cy, 3)
|
||||
|
||||
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
|
||||
|
||||
con.move(cy, 17)
|
||||
print(' ')
|
||||
let progressbar = '\x84196u'.repeat(paintWidth + 1)
|
||||
print(progressbar)
|
||||
|
||||
con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setPcmQueueCapacityIndex(0, 2) // queue size is now 8
|
||||
audio.setPcmQueueCapacityIndex(0, 2)
|
||||
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
audio.play(0)
|
||||
|
||||
if (interactive) {
|
||||
gui.audioInit({
|
||||
title: `${filebuf.file.name} TAD Q${firstMaxIndex} ${SAMPLE_RATE/1000}kHz`,
|
||||
tag: "TAD"
|
||||
})
|
||||
}
|
||||
|
||||
let stopPlay = false
|
||||
let errorlevel = 0
|
||||
|
||||
try {
|
||||
while (bytes_left > 0 && !stopPlay) {
|
||||
if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break }
|
||||
|
||||
if (interactive) {
|
||||
sys.poke(-40, 1)
|
||||
if (sys.peek(-41) == 67) { // Backspace key
|
||||
stopPlay = true
|
||||
}
|
||||
}
|
||||
const sampleCount = filebuf.readShort()
|
||||
const maxIndex = filebuf.readByte()
|
||||
const payloadSize = filebuf.readInt()
|
||||
|
||||
printPlayBar()
|
||||
|
||||
// Read TAD chunk header (standalone TAD format)
|
||||
// Format: [sample_count][max_index][payload_size][payload]
|
||||
let sampleCount = filebuf.readShort()
|
||||
let maxIndex = filebuf.readByte()
|
||||
let payloadSize = filebuf.readInt()
|
||||
|
||||
// Validate every chunk (not just first one)
|
||||
if (sampleCount < 0 || sampleCount > 65536) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid sample count ${sampleCount}. File may be corrupted.`)
|
||||
errorlevel = 1
|
||||
break
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid sample count ${sampleCount}.`)
|
||||
errorlevel = 1; break
|
||||
}
|
||||
if (maxIndex < 0 || maxIndex > 255) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid max index ${maxIndex}. File may be corrupted.`)
|
||||
errorlevel = 1
|
||||
break
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid max index ${maxIndex}.`)
|
||||
errorlevel = 1; break
|
||||
}
|
||||
if (payloadSize < 1 || payloadSize > 65536) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid payload size ${payloadSize}. File may be corrupted.`)
|
||||
errorlevel = 1
|
||||
break
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid payload size ${payloadSize}.`)
|
||||
errorlevel = 1; break
|
||||
}
|
||||
if (payloadSize + 7 > bytes_left) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Chunk size ${payloadSize + 7} exceeds remaining file size ${bytes_left}`)
|
||||
errorlevel = 1
|
||||
break
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Chunk size exceeds remaining file size.`)
|
||||
errorlevel = 1; break
|
||||
}
|
||||
|
||||
if (dumpCoeffs && chunkNumber < 3) {
|
||||
@@ -284,80 +156,59 @@ try {
|
||||
serial.println(` Sample Count: ${sampleCount}`)
|
||||
serial.println(` Max Index: ${maxIndex}`)
|
||||
serial.println(` Payload Size: ${payloadSize} bytes`)
|
||||
serial.println(` Bytes remaining in file: ${bytes_left}`)
|
||||
}
|
||||
|
||||
// Rewind 7 bytes to re-read the header along with payload
|
||||
// This allows reading the complete chunk (header + payload) in one call
|
||||
// Read entire chunk (header + payload) into TAD input buffer.
|
||||
filebuf.unread(7)
|
||||
filebuf.readBytes(7 + payloadSize, TAD_INPUT_ADDR)
|
||||
|
||||
// Read entire chunk (header + payload) to TAD input buffer
|
||||
// This matches TAV's approach for packet 0x24
|
||||
let totalChunkSize = 7 + payloadSize
|
||||
filebuf.readBytes(totalChunkSize, TAD_INPUT_ADDR)
|
||||
|
||||
if (dumpCoeffs && chunkNumber < 3) {
|
||||
// Dump first 32 bytes of compressed payload (skip 7-byte header)
|
||||
serial.print(` Compressed data (first 32 bytes): `)
|
||||
for (let i = 0; i < Math.min(32, payloadSize); i++) {
|
||||
let b = sys.peek(TAD_INPUT_ADDR + 7 + i)
|
||||
serial.print(`${(b & 0xFF).toString(16).padStart(2, '0')} `)
|
||||
}
|
||||
serial.println('')
|
||||
}
|
||||
|
||||
// Decode TAD chunk
|
||||
audio.tadDecode()
|
||||
|
||||
if (dumpCoeffs && chunkNumber < 3) {
|
||||
// After decoding, the decoded PCMu8 samples are in tadDecodedBin
|
||||
serial.println(` Decoded ${sampleCount} samples`)
|
||||
|
||||
// Dump first 16 decoded samples (PCMu8 stereo interleaved)
|
||||
serial.print(` Decoded (first 16 L samples): `)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2) & 0xFF} `)
|
||||
}
|
||||
serial.println('')
|
||||
serial.print(` Decoded (first 16 R samples): `)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2 + 1) & 0xFF} `)
|
||||
}
|
||||
serial.println('')
|
||||
serial.println('')
|
||||
}
|
||||
|
||||
// Upload decoded audio to queue
|
||||
audio.tadUploadDecoded(0, sampleCount)
|
||||
// After upload tadDecodedBin still holds the chunk until the next
|
||||
// tadDecode call, so it's safe to keep slicing samples out of it
|
||||
// during the playback wait below.
|
||||
|
||||
if (!dumpCoeffs) {
|
||||
// Sleep for the duration of the audio chunk to pace playback
|
||||
// This prevents uploading everything at once
|
||||
sys.sleep(bufRealTimeLen)
|
||||
// TAD chunks are typically 1 s long, so feeding the visualiser
|
||||
// once would freeze it for ~1 s. Walk the chunk in 2048-sample
|
||||
// slices (~64 ms each at 32 kHz) so the wavescope and XY-scope
|
||||
// stay in step with what the audio engine is actually playing.
|
||||
const chunkMs = Math.floor((sampleCount / SAMPLE_RATE) * 1000)
|
||||
const TAD_VIS_SLICE = 2048
|
||||
if (interactive) {
|
||||
gui.audioSetProgress(decodedLength / FILE_SIZE,
|
||||
bytesToSec(decodedLength), bytesToSec(FILE_SIZE))
|
||||
let sliceOff = 0
|
||||
while (sliceOff < sampleCount && !stopPlay) {
|
||||
if (gui.audioIsExitRequested()) { 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
|
||||
// direction for negative ptrs internally.
|
||||
gui.audioFeedPcm(TAD_DECODED_ADDR - sliceOff * 2, sliceN)
|
||||
gui.audioRender()
|
||||
sys.sleep(Math.floor((sliceN / SAMPLE_RATE) * 1000))
|
||||
sliceOff += sliceN
|
||||
}
|
||||
} else {
|
||||
sys.sleep(chunkMs)
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk size = header (7 bytes) + payload
|
||||
let chunkSize = 7 + payloadSize
|
||||
bytes_left -= chunkSize
|
||||
const chunkSize = 7 + payloadSize
|
||||
bytes_left -= chunkSize
|
||||
decodedLength += chunkSize
|
||||
chunkNumber++
|
||||
|
||||
// Limit coefficient dump to first 3 chunks
|
||||
if (dumpCoeffs && chunkNumber >= 3) {
|
||||
serial.println(`... (remaining chunks omitted)`)
|
||||
// Keep playing but don't dump more
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
printerrln(e)
|
||||
errorlevel = 1
|
||||
}
|
||||
finally {
|
||||
if (interactive) {
|
||||
con.move(cy + 3, 1)
|
||||
con.curs_set(1)
|
||||
}
|
||||
} finally {
|
||||
if (interactive) gui.audioClose()
|
||||
}
|
||||
|
||||
return errorlevel
|
||||
|
||||
@@ -512,7 +512,6 @@ function drawFrame() {
|
||||
colour(COL_LABEL, COL_BG)
|
||||
mvtext(ROW_TOP_BORDER, 4, ' TAUD ')
|
||||
colour(COL_DIM, COL_BG)
|
||||
mvtext(ROW_TOP_BORDER, COLS - 7, ' v0.1 ')
|
||||
|
||||
// Bottom border + exit hint.
|
||||
colour(COL_BORDER, COL_BG)
|
||||
@@ -725,7 +724,7 @@ function spawnEventsForRow(cueIdx, rowIdx) {
|
||||
note: note, pan: pan,
|
||||
ageFrames: 0,
|
||||
peakVol: 0,
|
||||
glyphSeed: (cueIdx * 64 + rowIdx + v * 13) & 0xFFFF
|
||||
glyphSeed: (cueIdx * 64 + rowIdx + v * 1280) & 0xFFFF
|
||||
}
|
||||
voiceLastNote[v] = note
|
||||
voiceLastInst[v] = effInst
|
||||
|
||||
@@ -1,329 +1,189 @@
|
||||
// usage: playwav audiofile.wav [/i]
|
||||
let fileeeee = files.open(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
let filename = fileeeee.fullPath
|
||||
// playwav — WAV (LPCM/ADPCM) player with the shared playgui visualiser.
|
||||
// Usage: playwav <file.wav> [-i]
|
||||
|
||||
const fileHandle = files.open(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
const filePath = fileHandle.fullPath
|
||||
|
||||
const WAV_FORMATS = ["LPCM", "ADPCM"]
|
||||
const WAV_CHANNELS = ["Mono", "Stereo", "3ch", "Quad", "4.1", "5.1", "6.1", "7.1"]
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() === "-i"
|
||||
|
||||
const seqread = require("seqread")
|
||||
const pcm = require("pcm")
|
||||
const gui = interactive ? require("playgui") : null
|
||||
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
|
||||
const WAV_FORMATS = ["LPCM", "ADPCM"]
|
||||
const WAV_CHANNELS = ["Mono", "Stereo", "3ch", "Quad", "4.1", "5.1", "6.1", "7.1"]
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
|
||||
const seqread = require("seqread")
|
||||
const pcm = require("pcm")
|
||||
|
||||
|
||||
|
||||
function printComments() {
|
||||
for (const [key, value] of Object.entries(comments)) {
|
||||
printdbg(`Wave Comment ${key}: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
function GCD(a, b) {
|
||||
a = Math.abs(a)
|
||||
b = Math.abs(b)
|
||||
if (b > a) {var temp = a; a = b; b = temp}
|
||||
a = Math.abs(a); b = Math.abs(b)
|
||||
if (b > a) { const t = a; a = b; b = t }
|
||||
while (true) {
|
||||
if (b == 0) return a
|
||||
if (b === 0) return a
|
||||
a %= b
|
||||
if (a == 0) return b
|
||||
if (a === 0) return b
|
||||
b %= a
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
|
||||
//println("Reading...")
|
||||
//serial.println("!!! READING")
|
||||
|
||||
seqread.prepare(filename)
|
||||
|
||||
|
||||
|
||||
|
||||
// decode header
|
||||
if (seqread.readFourCC() != "RIFF") {
|
||||
throw Error("File not RIFF")
|
||||
}
|
||||
|
||||
const FILE_SIZE = seqread.readInt() // size from "WAVEfmt"
|
||||
|
||||
if (seqread.readFourCC() != "WAVE") {
|
||||
throw Error("File is RIFF but not WAVE")
|
||||
}
|
||||
seqread.prepare(filePath)
|
||||
if (seqread.readFourCC() !== "RIFF") throw Error("File not RIFF")
|
||||
const FILE_SIZE = seqread.readInt()
|
||||
if (seqread.readFourCC() !== "WAVE") throw Error("File is RIFF but not WAVE")
|
||||
|
||||
let BLOCK_SIZE = 0
|
||||
let INFILE_BLOCK_SIZE = 0
|
||||
const QUEUE_MAX = 8 // according to the spec
|
||||
const QUEUE_MAX = 8
|
||||
|
||||
let pcmType;
|
||||
let nChannels;
|
||||
let samplingRate;
|
||||
let blockSize;
|
||||
let bitsPerSample;
|
||||
let byterate;
|
||||
let comments = {};
|
||||
let adpcmSamplesPerBlock;
|
||||
let readPtr = undefined
|
||||
let decodePtr = undefined
|
||||
let pcmType, nChannels, samplingRate, blockSize, bitsPerSample, byterate
|
||||
let adpcmSamplesPerBlock
|
||||
let readPtr, decodePtr
|
||||
const comments = {}
|
||||
|
||||
function bytesToSec(i) {
|
||||
if (adpcmSamplesPerBlock) {
|
||||
let newByteRate = samplingRate
|
||||
let generatedSamples = i / blockSize * adpcmSamplesPerBlock
|
||||
return generatedSamples / newByteRate
|
||||
}
|
||||
else {
|
||||
return i / byterate
|
||||
const generatedSamples = i / blockSize * adpcmSamplesPerBlock
|
||||
return generatedSamples / samplingRate
|
||||
}
|
||||
return i / byterate
|
||||
}
|
||||
function secToReadable(n) {
|
||||
let mins = ''+((n/60)|0)
|
||||
let secs = ''+(n % 60)
|
||||
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
|
||||
}
|
||||
|
||||
function checkIfPlayable() {
|
||||
if (pcmType != 1 && pcmType != 2) return `PCM Type not LPCM/ADPCM (${pcmType})`
|
||||
if (pcmType !== 1 && pcmType !== 2) return `PCM Type not LPCM/ADPCM (${pcmType})`
|
||||
if (nChannels < 1 || nChannels > 2) return `Audio not mono/stereo but instead has ${nChannels} channels`
|
||||
if (pcmType != 1 && samplingRate != pcm.HW_SAMPLING_RATE) return `Format is ADPCM but sampling rate is not ${pcm.HW_SAMPLING_RATE}: ${samplingRate}`
|
||||
if (pcmType !== 1 && samplingRate !== pcm.HW_SAMPLING_RATE)
|
||||
return `Format is ADPCM but sampling rate is not ${pcm.HW_SAMPLING_RATE}: ${samplingRate}`
|
||||
return "playable!"
|
||||
}
|
||||
// @return decoded sample length (not count!)
|
||||
|
||||
function decodeInfilePcm(inPtr, outPtr, inputLen) {
|
||||
// LPCM
|
||||
if (1 == pcmType)
|
||||
if (pcmType === 1)
|
||||
return pcm.decodeLPCM(inPtr, outPtr, inputLen, { nChannels, bitsPerSample, samplingRate, blockSize })
|
||||
else if (2 == pcmType)
|
||||
if (pcmType === 2)
|
||||
return pcm.decodeMS_ADPCM(inPtr, outPtr, inputLen, { nChannels })
|
||||
else
|
||||
throw Error(`PCM Type not LPCM or ADPCM (${pcmType})`)
|
||||
throw Error(`PCM Type not LPCM or ADPCM (${pcmType})`)
|
||||
}
|
||||
|
||||
let stopPlay = false
|
||||
|
||||
|
||||
con.curs_set(0)
|
||||
let [__, CONSOLE_WIDTH] = con.getmaxyx()
|
||||
function printPlayerShell() {
|
||||
if (interactive) {
|
||||
let [cy, cx] = con.getyx()
|
||||
// file name
|
||||
con.mvaddch(cy, 1)
|
||||
con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5)
|
||||
print(fileeeee.name)
|
||||
con.prnch(0xC6);con.prnch(0xCD)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 26 - fileeeee.name.length))
|
||||
con.prnch(0xB5)
|
||||
print("Hold Bksp to Exit")
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB)
|
||||
|
||||
// L R pillar
|
||||
con.prnch(0xBA)
|
||||
con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA)
|
||||
|
||||
// media info
|
||||
let mediaInfoStr = `WAV ${WAV_FORMATS[pcmType-1]} ${WAV_CHANNELS[nChannels-1]} ${byterate*0.008*(pcmType == 2 ? 2 : 1)}kbps`
|
||||
con.move(cy+2,1)
|
||||
con.prnch(0xC8)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length))
|
||||
con.prnch(0xB5)
|
||||
print(mediaInfoStr)
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC)
|
||||
|
||||
con.move(cy+1, 2)
|
||||
}
|
||||
}
|
||||
let [cy, cx] = con.getyx(); cy++
|
||||
let paintWidth = CONSOLE_WIDTH - 20
|
||||
function printPlayBar(startOffset) {
|
||||
if (interactive) {
|
||||
let currently = seqread.getReadCount() - startOffset
|
||||
let total = FILE_SIZE - startOffset - 8
|
||||
|
||||
let currentlySec = Math.round(bytesToSec(currently))
|
||||
let totalSec = Math.round(bytesToSec(total))
|
||||
|
||||
con.move(cy, 3)
|
||||
print(' '.repeat(15))
|
||||
con.move(cy, 3)
|
||||
|
||||
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
|
||||
|
||||
con.move(cy, 17)
|
||||
print(' ')
|
||||
let progressbar = '\x84196u'.repeat(paintWidth + 1)
|
||||
print(progressbar)
|
||||
|
||||
con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB)
|
||||
}
|
||||
}
|
||||
let errorlevel = 0
|
||||
// read chunks loop
|
||||
try {
|
||||
while (!stopPlay && seqread.getReadCount() < FILE_SIZE - 8) {
|
||||
let chunkName = seqread.readFourCC()
|
||||
let chunkSize = seqread.readInt()
|
||||
printdbg(`Reading '${chunkName}' at ${seqread.getReadCount() - 8}`)
|
||||
|
||||
// here be lotsa if-else
|
||||
if ("fmt " == chunkName) {
|
||||
pcmType = seqread.readShort()
|
||||
nChannels = seqread.readShort()
|
||||
samplingRate = seqread.readInt()
|
||||
byterate = seqread.readInt()
|
||||
blockSize = seqread.readShort()
|
||||
bitsPerSample = seqread.readShort()
|
||||
if (pcmType != 2) {
|
||||
seqread.skip(chunkSize - 16)
|
||||
try {
|
||||
while (!stopPlay && seqread.getReadCount() < FILE_SIZE - 8) {
|
||||
const chunkName = seqread.readFourCC()
|
||||
const chunkSize = seqread.readInt()
|
||||
printdbg(`Reading '${chunkName}' at ${seqread.getReadCount() - 8}`)
|
||||
|
||||
if (chunkName === "fmt ") {
|
||||
pcmType = seqread.readShort()
|
||||
nChannels = seqread.readShort()
|
||||
samplingRate = seqread.readInt()
|
||||
byterate = seqread.readInt()
|
||||
blockSize = seqread.readShort()
|
||||
bitsPerSample = seqread.readShort()
|
||||
if (pcmType !== 2) {
|
||||
seqread.skip(chunkSize - 16)
|
||||
} else {
|
||||
seqread.skip(2)
|
||||
adpcmSamplesPerBlock = seqread.readShort()
|
||||
seqread.skip(chunkSize - (16 + 4))
|
||||
}
|
||||
|
||||
if (pcmType === 1) {
|
||||
const incr = LCM(blockSize, samplingRate / GCD(samplingRate, pcm.HW_SAMPLING_RATE))
|
||||
while (BLOCK_SIZE < 4096) BLOCK_SIZE += incr
|
||||
INFILE_BLOCK_SIZE = BLOCK_SIZE * bitsPerSample / 8
|
||||
} else if (pcmType === 2) {
|
||||
BLOCK_SIZE = blockSize
|
||||
INFILE_BLOCK_SIZE = BLOCK_SIZE
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
const tag = "WAV"
|
||||
const title = fileHandle.name +
|
||||
` ${WAV_FORMATS[pcmType-1]} ${WAV_CHANNELS[nChannels-1]} ${byterate*0.008*(pcmType === 2 ? 2 : 1)}kbps`
|
||||
gui.audioInit({ title, tag })
|
||||
}
|
||||
}
|
||||
else if (chunkName === "LIST") {
|
||||
const startOffset = seqread.getReadCount()
|
||||
const subChunkName = seqread.readFourCC()
|
||||
while (seqread.getReadCount() < startOffset + chunkSize) {
|
||||
if (subChunkName === "INFO") {
|
||||
let key = seqread.readFourCC()
|
||||
let valueLen = seqread.readInt()
|
||||
while (key.charCodeAt(0) === 0) {
|
||||
const kbytes = [key.charCodeAt(1), key.charCodeAt(2), key.charCodeAt(3), valueLen & 255]
|
||||
const klen = [(valueLen >>> 8) & 255, (valueLen >>> 16) & 255, (valueLen >>> 24) & 255, seqread.readOneByte()]
|
||||
key = String.fromCharCode.apply(null, kbytes)
|
||||
valueLen = klen[0] | (klen[1] << 8) | (klen[2] << 16) | (klen[3] << 24)
|
||||
}
|
||||
comments[key] = seqread.readString(valueLen)
|
||||
} else {
|
||||
seqread.skip(startOffset + chunkSize - seqread.getReadCount())
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (chunkName === "data") {
|
||||
const startOffset = seqread.getReadCount()
|
||||
const reason = checkIfPlayable()
|
||||
if (reason !== "playable!") throw Error("WAVE not playable: " + reason)
|
||||
|
||||
readPtr = sys.malloc(pcmType === 2 ? BLOCK_SIZE : BLOCK_SIZE * bitsPerSample / 8)
|
||||
decodePtr = sys.malloc(BLOCK_SIZE * pcm.HW_SAMPLING_RATE / samplingRate)
|
||||
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
|
||||
let readLength = 1
|
||||
while (!stopPlay && seqread.getReadCount() < startOffset + chunkSize && readLength > 0) {
|
||||
if (interactive && gui.audioIsExitRequested()) { stopPlay = true; break }
|
||||
|
||||
if (audio.getPosition(0) <= 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(0, decodePtr, decodedSampleLength, 0)
|
||||
audio.setSampleUploadLength(0, decodedSampleLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
sys.spin()
|
||||
}
|
||||
audio.play(0)
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
const cur = seqread.getReadCount() - startOffset
|
||||
const tot = FILE_SIZE - startOffset - 8
|
||||
gui.audioSetProgress(cur / tot, bytesToSec(cur), bytesToSec(tot))
|
||||
gui.audioRender()
|
||||
}
|
||||
sys.sleep(10)
|
||||
}
|
||||
}
|
||||
else {
|
||||
seqread.skip(2)
|
||||
adpcmSamplesPerBlock = seqread.readShort()
|
||||
seqread.skip(chunkSize - (16 + 4))
|
||||
seqread.skip(chunkSize)
|
||||
}
|
||||
|
||||
// define BLOCK_SIZE as integer multiple of blockSize, for LPCM
|
||||
// ADPCM will be decoded per-block basis
|
||||
if (1 == pcmType) {
|
||||
// get GCD of given values; this wll make resampling headache-free
|
||||
let blockSizeIncrement = LCM(blockSize, samplingRate / GCD(samplingRate, pcm.HW_SAMPLING_RATE))
|
||||
|
||||
while (BLOCK_SIZE < 4096) {
|
||||
BLOCK_SIZE += blockSizeIncrement // for rate 44100, BLOCK_SIZE will be 4116
|
||||
}
|
||||
INFILE_BLOCK_SIZE = BLOCK_SIZE * bitsPerSample / 8 // for rate 44100, INFILE_BLOCK_SIZE will be 8232
|
||||
}
|
||||
else if (2 == pcmType) {
|
||||
BLOCK_SIZE = blockSize
|
||||
INFILE_BLOCK_SIZE = BLOCK_SIZE
|
||||
}
|
||||
|
||||
printdbg(`Format: ${pcmType}, Channels: ${nChannels}, Rate: ${samplingRate}, BitDepth: ${bitsPerSample}`)
|
||||
printdbg(`BLOCK_SIZE=${BLOCK_SIZE}, INFILE_BLOCK_SIZE=${INFILE_BLOCK_SIZE}`)
|
||||
printPlayerShell()
|
||||
sys.spin()
|
||||
}
|
||||
else if ("LIST" == chunkName) {
|
||||
let startOffset = seqread.getReadCount()
|
||||
let subChunkName = seqread.readFourCC()
|
||||
while (seqread.getReadCount() < startOffset + chunkSize) {
|
||||
if ("INFO" == subChunkName) {
|
||||
let key = seqread.readFourCC()
|
||||
let valueLen = seqread.readInt()
|
||||
|
||||
// f-you WAVE encoders with nonstandard behaviours
|
||||
// related: https://stackoverflow.com/questions/49537639/riff-icmt-tag-size-doesnt-seem-to-match-data
|
||||
while (0 == key.charCodeAt(0)) {
|
||||
printdbg(`Previous key had more zero bytes padded than its marked length, skipping one byte...`)
|
||||
|
||||
let kbytes = [key.charCodeAt(1), key.charCodeAt(2), key.charCodeAt(3), valueLen & 255]
|
||||
let klen = [(valueLen >>> 8) & 255, (valueLen >>> 16) & 255, (valueLen >>> 24) & 255, seqread.readOneByte()]
|
||||
|
||||
key = String.fromCharCode.apply(null, kbytes)
|
||||
valueLen = klen[0] | (klen[1] << 8) | (klen[2] << 16) | (klen[3] << 24)
|
||||
}
|
||||
|
||||
printdbg(`Reading LIST INFO ${key}[${[0,1,2,3].map((i)=>"0x"+key.charCodeAt(i).toString(16).padStart(2,'0'))}] (${valueLen} bytes): `)
|
||||
|
||||
|
||||
let value = seqread.readString(valueLen)
|
||||
printdbg(" |"+value)
|
||||
comments[key] = value
|
||||
}
|
||||
else {
|
||||
printdbg(`LIST skip subchunk ${subChunkName} (${startOffset + chunkSize - seqread.getReadCount()} bytes)`)
|
||||
seqread.skip(startOffset + chunkSize - seqread.getReadCount())
|
||||
}
|
||||
}
|
||||
printComments()
|
||||
}
|
||||
else if ("data" == chunkName) {
|
||||
let startOffset = seqread.getReadCount()
|
||||
|
||||
printdbg(`WAVE size: ${chunkSize}, startOffset=${startOffset}`)
|
||||
// check if the format is actually playable
|
||||
let unplayableReason = checkIfPlayable()
|
||||
if (unplayableReason != "playable!") throw Error("WAVE not playable: "+unplayableReason)
|
||||
|
||||
if (pcmType == 2)
|
||||
readPtr = sys.malloc(BLOCK_SIZE)
|
||||
else
|
||||
readPtr = sys.malloc(BLOCK_SIZE * bitsPerSample / 8)
|
||||
|
||||
decodePtr = sys.malloc(BLOCK_SIZE * pcm.HW_SAMPLING_RATE / samplingRate)
|
||||
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
|
||||
let readLength = 1
|
||||
while (!stopPlay && seqread.getReadCount() < startOffset + chunkSize && readLength > 0) {
|
||||
if (interactive) {
|
||||
sys.poke(-40, 1)
|
||||
if (sys.peek(-41) == 67) {
|
||||
stopPlay = true
|
||||
}
|
||||
}
|
||||
|
||||
printPlayBar(startOffset)
|
||||
|
||||
let queueSize = audio.getPosition(0)
|
||||
if (queueSize <= 1) {
|
||||
|
||||
|
||||
// upload four samples for lag-safely
|
||||
for (let repeat = 0; repeat < QUEUE_MAX; repeat++) {
|
||||
let remainingBytes = FILE_SIZE - 8 - seqread.getReadCount()
|
||||
|
||||
readLength = (remainingBytes < INFILE_BLOCK_SIZE) ? remainingBytes : INFILE_BLOCK_SIZE
|
||||
if (readLength <= 0) {
|
||||
printdbg(`readLength = ${readLength}`)
|
||||
break
|
||||
}
|
||||
|
||||
printdbg(`offset: ${seqread.getReadCount()}/${FILE_SIZE + 8}; readLength: ${readLength}`)
|
||||
|
||||
seqread.readBytes(readLength, readPtr)
|
||||
|
||||
let decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength)
|
||||
printdbg(` decodedSampleLength: ${decodedSampleLength}`)
|
||||
|
||||
audio.putPcmDataByPtr(0, decodePtr, decodedSampleLength, 0)
|
||||
audio.setSampleUploadLength(0, decodedSampleLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
sys.spin()
|
||||
}
|
||||
|
||||
audio.play(0)
|
||||
}
|
||||
|
||||
let remainingBytes = FILE_SIZE - 8 - seqread.getReadCount()
|
||||
printdbg(`readLength = ${readLength}; remainingBytes2 = ${remainingBytes}; seqread.getReadCount() = ${seqread.getReadCount()}; startOffset + chunkSize = ${startOffset + chunkSize}`)
|
||||
|
||||
|
||||
sys.sleep(10)
|
||||
}
|
||||
}
|
||||
else {
|
||||
seqread.skip(chunkSize)
|
||||
}
|
||||
|
||||
|
||||
let remainingBytes = FILE_SIZE - 8 - seqread.getReadCount()
|
||||
printdbg(`remainingBytes2 = ${remainingBytes}`)
|
||||
sys.spin()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
printerrln(e)
|
||||
errorlevel = 1
|
||||
}
|
||||
finally {
|
||||
//audio.stop(0)
|
||||
if (readPtr !== undefined) sys.free(readPtr)
|
||||
} finally {
|
||||
if (readPtr !== undefined) sys.free(readPtr)
|
||||
if (decodePtr !== undefined) sys.free(decodePtr)
|
||||
if (interactive) gui.audioClose()
|
||||
}
|
||||
|
||||
return errorlevel
|
||||
|
||||
Reference in New Issue
Block a user