mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
399 lines
14 KiB
JavaScript
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 |