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

123 lines
4.3 KiB
JavaScript

// 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_CHANNELMODES = ["Stereo", "Joint", "Dual", "Mono"]
const pcm = require("pcm")
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) }
get fileHeader() { return this.seq.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 = 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 decodedLength = 0
const bufRealTimeLen = 36 // one MP2 frame at 32 kHz ≈ 36 ms
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setPcmQueueCapacityIndex(0, 2)
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
audio.setMasterVolume(0, 255)
audio.play(0)
audio.mp2Init()
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 && 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)) {
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
decodedLength += FRAME_SIZE
}
} catch (e) {
printerrln(e)
errorlevel = 1
} finally {
if (interactive) {
if (mp2VisScratch) sys.free(mp2VisScratch)
gui.audioClose()
}
}
return errorlevel