Files
tsvm/assets/disk0/tvdos/bin/playmv1.js
2025-09-17 00:55:23 +09:00

399 lines
14 KiB
JavaScript

// usage: playmv1 moviefile.mv1 [/i]
const SND_BASE_ADDR = audio.getBaseAddr()
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const WIDTH = 560
const HEIGHT = 448
const FBUF_SIZE = WIDTH * HEIGHT
const AUTO_BGCOLOUR_CHANGE = true
const MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x4D, 0x4F, 0x56]
const pcm = require("pcm")
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
const FILE_LENGTH = files.open(fullFilePath.full).size
let videoRateBin = []
con.clear();con.curs_set(0)
graphics.clearPixels(255)
graphics.clearPixels2(240)
let seqread = undefined
let fullFilePathStr = fullFilePath.full
// select seqread driver to use
if (fullFilePathStr.startsWith('$:/TAPE') || fullFilePathStr.startsWith('$:\\TAPE')) {
seqread = require("seqreadtape")
}
else {
seqread = require("seqread")
}
seqread.prepare(fullFilePathStr)
let magic = seqread.readBytes(8)
let magicMatching = true
let actualMagic = []
// check if magic number matches
MAGIC.forEach((b,i) => {
let testb = sys.peek(magic + i) & 255 // for some reason this must be located here
actualMagic.push(testb)
if (testb != b) {
magicMatching = false
}
})
sys.free(magic)
if (!magicMatching) {
println("Not a movie file (MAGIC mismatch) -- got " + actualMagic.join())
return 1
}
let mp2Initialised = false
let width = seqread.readShort()
let height = seqread.readShort()
let fps = seqread.readShort(); if (fps == 0) fps = 9999
function updateDataRateBin(rate) {
videoRateBin.push(rate)
if (videoRateBin.length > fps) {
videoRateBin.shift()
}
}
function getVideoRate(rate) {
let baseRate = videoRateBin.reduce((a, c) => a + c, 0)
let mult = fps / videoRateBin.length
return baseRate * mult
}
//fps = 9999
const FRAME_TIME = 1.0 / fps
const FRAME_COUNT = seqread.readInt() % 16777216
seqread.readShort() // skip unused field
const audioQueueInfo = seqread.readShort()
const AUDIO_QUEUE_LENGTH = (audioQueueInfo >> 12) + 1
const AUDIO_QUEUE_BYTES = (audioQueueInfo & 0xFFF) << 2
seqread.skip(10) // skip 12 bytes
let audioQueuePos = 0
let akku = FRAME_TIME
let framesRendered = 0
//serial.println(seqread.getReadCount()) // must say 18
//serial.println(`Dim: (${width}x${height}), FPS: ${fps}, Frames: ${FRAME_COUNT}`)
/*if (type != 4 && type != 5 && type != 260 && type != 261) {
printerrln("Not an iPF mov")
return 1
}*/
let ipfbuf = sys.malloc(FBUF_SIZE)
graphics.setGraphicsMode(4)
let startTime = sys.nanoTime()
let framesRead = 0
let audioFired = false
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setMasterVolume(0, 255)
function s16StTou8St(inPtrL, inPtrR, outPtr, length) {
for (let k = 0; k < length; k+=2) {
let sample1 = pcm.u16Tos16(sys.peek(inPtrL + k + 0) | (sys.peek(inPtrL + k + 1) << 8))
let sample2 = pcm.u16Tos16(sys.peek(inPtrR + k + 0) | (sys.peek(inPtrR + k + 1) << 8))
sys.poke(outPtr + k, pcm.s16Tou8(sample1))
sys.poke(outPtr + k + 1, pcm.s16Tou8(sample2))
}
}
function getRGBfromScr(x, y) {
let offset = y * WIDTH + x
let rg = sys.peek(-1048577 - offset)
let ba = sys.peek(-1310721 - offset)
return [(rg >>> 4) / 15.0, (rg & 15) / 15.0, (ba >>> 4) / 15.0]
}
const BIAS_LIGHTING_MIN = 1.0 / 16.0
let oldBgcol = [BIAS_LIGHTING_MIN, BIAS_LIGHTING_MIN, BIAS_LIGHTING_MIN]
let stopPlay = false
if (interactive) {
con.move(1,1)
println("Push and hold Backspace to exit")
}
let notifHideTimer = 0
const NOTIF_SHOWUPTIME = 3000000000
let [cy, cx] = con.getyx()
let doFrameskip = true
let errorlevel = 0
try {
let t1 = sys.nanoTime()
renderLoop:
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
if (akku >= FRAME_TIME) {
let frameUnit = 0 // 0: no decode, 1: normal playback, 2+: skip (n-1) frames
while (!stopPlay && akku >= FRAME_TIME) {
if (interactive) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) {
stopPlay = true
}
}
akku -= FRAME_TIME
frameUnit += 1
}
if (!doFrameskip) frameUnit = 1
if (frameUnit != 0) {
// skip frames if necessary
while (!stopPlay && frameUnit >= 1 && seqread.getReadCount() < FILE_LENGTH) {
if (interactive) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) {
stopPlay = true
}
}
let packetType = seqread.readShort()
// ideally, first two packets will be audio packets
// sync packets
if (65535 == packetType) {
frameUnit -= 1
}
// background colour packets
else if (65279 == packetType) {
AUTO_BGCOLOUR_CHANGE = false
let rgbx = seqread.readInt()
graphics.setBackground(
(rgbx & 0xFF000000) >>> 24,
(rgbx & 0x00FF0000) >>> 16,
(rgbx & 0x0000FF00) >>> 8
)
}
// video packets
else if (packetType < 2047) {
// iPF
if (packetType == 4 || packetType == 5 || packetType == 260 || packetType == 261) {
let decodefun = (packetType > 255) ? graphics.decodeIpf2 : graphics.decodeIpf1
let payloadLen = seqread.readInt()
updateDataRateBin(payloadLen)
if (framesRead >= FRAME_COUNT) {
break renderLoop
}
framesRead += 1
let gzippedPtr = seqread.readBytes(payloadLen)
framesRendered += 1
if (frameUnit == 1) {
gzip.decompFromTo(gzippedPtr, payloadLen, ipfbuf) // should return FBUF_SIZE
decodefun(ipfbuf, -1048577, -1310721, width, height, (packetType & 255) == 5)
// defer audio playback until a first frame is sent
if (!audioFired) {
audio.play(0)
audioFired = true
}
// calculate bgcolour from the edges of the screen
if (AUTO_BGCOLOUR_CHANGE) {
let samples = []
for (let x = 8; x < 560; x+=32) {
samples.push(getRGBfromScr(x, 3))
samples.push(getRGBfromScr(x, 445))
}
for (let y = 29; y < 448; y+=26) {
samples.push(getRGBfromScr(8, y))
samples.push(getRGBfromScr(552, y))
}
let out = [0.0, 0.0, 0.0]
samples.forEach(rgb=>{
out[0] += rgb[0]
out[1] += rgb[1]
out[2] += rgb[2]
})
out[0] = BIAS_LIGHTING_MIN + (out[0] / samples.length / 2.0) // darken a bit
out[1] = BIAS_LIGHTING_MIN + (out[1] / samples.length / 2.0)
out[2] = BIAS_LIGHTING_MIN + (out[2] / samples.length / 2.0)
let bgr = (oldBgcol[0]*5 + out[0]) / 6.0
let bgg = (oldBgcol[1]*5 + out[1]) / 6.0
let bgb = (oldBgcol[2]*5 + out[2]) / 6.0
oldBgcol = [bgr, bgg, bgb]
graphics.setBackground(Math.round(bgr * 255), Math.round(bgg * 255), Math.round(bgb * 255))
}
}
sys.free(gzippedPtr)
}
// iPF1d
else if (packetType == 516) {
doFrameskip = false // disable frameskip for delta-coding
let payloadLen = seqread.readInt()
updateDataRateBin(payloadLen)
if (framesRead >= FRAME_COUNT) {
break renderLoop
}
framesRead += 1
let gzippedPtr = seqread.readBytes(payloadLen)
framesRendered += 1
if (frameUnit == 1) {
gzip.decompFromTo(gzippedPtr, payloadLen, ipfbuf) // should return FBUF_SIZE
graphics.applyIpf1d(ipfbuf, -1048577, -1310721, width, height)
// defer audio playback until a first frame is sent
if (!audioFired) {
audio.play(0)
audioFired = true
}
// calculate bgcolour from the edges of the screen
if (AUTO_BGCOLOUR_CHANGE) {
let samples = []
for (let x = 8; x < 560; x+=32) {
samples.push(getRGBfromScr(x, 3))
samples.push(getRGBfromScr(x, 445))
}
for (let y = 29; y < 448; y+=26) {
samples.push(getRGBfromScr(8, y))
samples.push(getRGBfromScr(552, y))
}
let out = [0.0, 0.0, 0.0]
samples.forEach(rgb=>{
out[0] += rgb[0]
out[1] += rgb[1]
out[2] += rgb[2]
})
out[0] = BIAS_LIGHTING_MIN + (out[0] / samples.length / 2.0) // darken a bit
out[1] = BIAS_LIGHTING_MIN + (out[1] / samples.length / 2.0)
out[2] = BIAS_LIGHTING_MIN + (out[2] / samples.length / 2.0)
let bgr = (oldBgcol[0]*5 + out[0]) / 6.0
let bgg = (oldBgcol[1]*5 + out[1]) / 6.0
let bgb = (oldBgcol[2]*5 + out[2]) / 6.0
oldBgcol = [bgr, bgg, bgb]
graphics.setBackground(Math.round(bgr * 255), Math.round(bgg * 255), Math.round(bgb * 255))
}
}
sys.free(gzippedPtr)
}
else {
throw Error(`Unknown Video Packet with type ${packetType} at offset ${seqread.getReadCount() - 2}`)
}
}
// audio packets
else if (4096 <= packetType && packetType <= 6143) {
let readLength = (packetType >>> 8 == 17) ?
MP2_FRAME_SIZE[(packetType & 255) >>> 1] // if the packet is MP2, deduce it from the packet type
: seqread.readInt() // else, read 4 more bytes
if (readLength == 0) throw Error("Readlength is zero")
// MP2
if (packetType >>> 8 == 17) {
if (!mp2Initialised) {
mp2Initialised = true
audio.mp2Init()
}
seqread.readBytes(readLength, SND_BASE_ADDR - 2368)
audio.mp2Decode()
audio.mp2UploadDecoded(0)
}
// RAW PCM packets (decode on the fly)
else if (packetType == 0x1000 || packetType == 0x1001) {
let frame = seqread.readBytes(readLength)
audio.putPcmDataByPtr(frame, readLength, 0)
audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0)
sys.free(frame)
}
else {
throw Error(`Audio Packet with type ${packetType} at offset ${seqread.getReadCount() - 2}`)
}
}
else {
println(`Unknown Packet with type ${packetType} at offset ${seqread.getReadCount() - 2}`)
}
}
}
else {
serial.println(`frameunit ${frameUnit}`)
framesRendered += 1
}
}
sys.sleep(1)
let t2 = sys.nanoTime()
akku += (t2 - t1) / 1000000000.0
if (interactive) {
notifHideTimer += (t2 - t1)
if (notifHideTimer > (NOTIF_SHOWUPTIME + FRAME_TIME)) {
con.clear()
}
con.move(32, 1)
graphics.setTextFore(161)
print(`VRate: ${(getVideoRate() / 1024 * 8)|0} kbps `)
con.move(1, 1)
}
t1 = t2
}
}
catch (e) {
printerrln(e)
errorlevel = 1
}
finally {
let endTime = sys.nanoTime()
sys.free(ipfbuf)
if (AUDIO_QUEUE_BYTES > 0 && AUDIO_QUEUE_LENGTH > 1) {
}
//audio.stop(0)
let timeTook = (endTime - startTime) / 1000000000.0
//println(`Actual FPS: ${framesRendered / timeTook}`)
audio.stop(0)
audio.purgeQueue(0)
if (interactive) {
con.clear()
}
}
return errorlevel