Files
tsvm/assets/disk0/tvdos/bin/playwav.js
2026-05-25 14:24:32 +09:00

190 lines
7.3 KiB
JavaScript

// 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) }
function GCD(a, b) {
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
a %= b
if (a === 0) return b
b %= a
}
}
function LCM(a, b) { return (!a || !b) ? 0 : Math.abs((a * b) / GCD(a, b)) }
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
let pcmType, nChannels, samplingRate, blockSize, bitsPerSample, byterate
let adpcmSamplesPerBlock
let readPtr, decodePtr
const comments = {}
function bytesToSec(i) {
if (adpcmSamplesPerBlock) {
const generatedSamples = i / blockSize * adpcmSamplesPerBlock
return generatedSamples / samplingRate
}
return i / byterate
}
function checkIfPlayable() {
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}`
return "playable!"
}
function decodeInfilePcm(inPtr, outPtr, inputLen) {
if (pcmType === 1)
return pcm.decodeLPCM(inPtr, outPtr, inputLen, { nChannels, bitsPerSample, samplingRate, blockSize })
if (pcmType === 2)
return pcm.decodeMS_ADPCM(inPtr, outPtr, inputLen, { nChannels })
throw Error(`PCM Type not LPCM or ADPCM (${pcmType})`)
}
let stopPlay = false
let errorlevel = 0
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(chunkSize)
}
sys.spin()
}
} catch (e) {
printerrln(e)
errorlevel = 1
} finally {
if (readPtr !== undefined) sys.free(readPtr)
if (decodePtr !== undefined) sys.free(decodePtr)
if (interactive) gui.audioClose()
}
return errorlevel