mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
// usage: playwav audiofile.wav [/i]
|
|
let fileeeee = files.open(_G.shell.resolvePathInput(exec_args[1]).full)
|
|
let filename = fileeeee.fullPath
|
|
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}
|
|
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")
|
|
|
|
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")
|
|
}
|
|
|
|
let BLOCK_SIZE = 0
|
|
let INFILE_BLOCK_SIZE = 0
|
|
const QUEUE_MAX = 8 // according to the spec
|
|
|
|
let pcmType;
|
|
let nChannels;
|
|
let samplingRate;
|
|
let blockSize;
|
|
let bitsPerSample;
|
|
let byterate;
|
|
let comments = {};
|
|
let adpcmSamplesPerBlock;
|
|
let readPtr = undefined
|
|
let decodePtr = undefined
|
|
|
|
function bytesToSec(i) {
|
|
if (adpcmSamplesPerBlock) {
|
|
let newByteRate = samplingRate
|
|
let generatedSamples = i / blockSize * adpcmSamplesPerBlock
|
|
return generatedSamples / newByteRate
|
|
}
|
|
else {
|
|
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 (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!"
|
|
}
|
|
// @return decoded sample length (not count!)
|
|
function decodeInfilePcm(inPtr, outPtr, inputLen) {
|
|
// LPCM
|
|
if (1 == pcmType)
|
|
return pcm.decodeLPCM(inPtr, outPtr, inputLen, { nChannels, bitsPerSample, samplingRate, blockSize })
|
|
else if (2 == pcmType)
|
|
return pcm.decodeMS_ADPCM(inPtr, outPtr, inputLen, { nChannels })
|
|
else
|
|
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)
|
|
}
|
|
else {
|
|
seqread.skip(2)
|
|
adpcmSamplesPerBlock = seqread.readShort()
|
|
seqread.skip(chunkSize - (16 + 4))
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
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(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) {
|
|
printerrln(e)
|
|
errorlevel = 1
|
|
}
|
|
finally {
|
|
//audio.stop(0)
|
|
if (readPtr !== undefined) sys.free(readPtr)
|
|
if (decodePtr !== undefined) sys.free(decodePtr)
|
|
}
|
|
|
|
return errorlevel
|