mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 14:24:05 +09:00
449 lines
22 KiB
JavaScript
449 lines
22 KiB
JavaScript
/*
|
||
* mediadec_common.mjs — shared front-end utilities for the mediadec library.
|
||
*
|
||
* Holds everything the three movie backends (iPF/MOV, TEV, TAV) duplicated in
|
||
* the old standalone players: magic constants, packet-type / SSF-opcode tables,
|
||
* the TAV quality LUT, seqread selection, the audio router, the subtitle
|
||
* engine, bias lighting, and the `sampleGray` / `sampleColour` source samplers
|
||
* used by the player's ASCII-render path — both a *Screen pair (read the GPU
|
||
* display planes, for iPF) and a *RGB pair (read a RAM RGB888 frame, for the
|
||
* decode-into-RAM backends TEV / TAV).
|
||
*
|
||
* Runs in the same GraalVM context as the player, so the host globals
|
||
* (sys/graphics/audio/con/serial/files/gzip) are visible directly, exactly as
|
||
* in seqread.mjs / playgui.mjs.
|
||
*/
|
||
|
||
// ── Magic numbers ───────────────────────────────────────────────────────────
|
||
const MAGIC_MOV = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x4D, 0x4F, 0x56] // "\x1FTSVMMOV"
|
||
const MAGIC_TEV = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x45, 0x56] // "\x1FTSVMTEV"
|
||
const MAGIC_TAV = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVMTAV"
|
||
const MAGIC_TAP = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x50] // "\x1FTSVMTAP"
|
||
const MAGIC_UCF = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVMUCF"
|
||
|
||
// ── MP2 frame-size table (shared by iPF/TEV/TAV) ────────────────────────────
|
||
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
|
||
|
||
// ── SSF subtitle opcodes (shared) ───────────────────────────────────────────
|
||
const SSF_OP_NOP = 0x00
|
||
const SSF_OP_SHOW = 0x01
|
||
const SSF_OP_HIDE = 0x02
|
||
const SSF_OP_MOVE = 0x03
|
||
const SSF_OP_UPLOAD_LOW_FONT = 0x80
|
||
const SSF_OP_UPLOAD_HIGH_FONT = 0x81
|
||
|
||
// ── TAV quality LUT (index → quantiser) ─────────────────────────────────────
|
||
const QLUT = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,264,272,280,288,296,304,312,320,328,336,344,352,360,368,376,384,392,400,408,416,424,432,440,448,456,464,472,480,488,496,504,512,528,544,560,576,592,608,624,640,656,672,688,704,720,736,752,768,784,800,816,832,848,864,880,896,912,928,944,960,976,992,1008,1024,1056,1088,1120,1152,1184,1216,1248,1280,1312,1344,1376,1408,1440,1472,1504,1536,1568,1600,1632,1664,1696,1728,1760,1792,1824,1856,1888,1920,1952,1984,2016,2048,2112,2176,2240,2304,2368,2432,2496,2560,2624,2688,2752,2816,2880,2944,3008,3072,3136,3200,3264,3328,3392,3456,3520,3584,3648,3712,3776,3840,3904,3968,4032,4096]
|
||
|
||
// ── Display-plane addresses (4bpp / mode 4) ─────────────────────────────────
|
||
const DISP_RG = -1048577
|
||
const DISP_BA = -1310721
|
||
const DISP_PLANE3 = -1310721 - 262144 // mode-8 third plane base (for getRGBfromScr)
|
||
|
||
// ── seqread selection ───────────────────────────────────────────────────────
|
||
// Mirrors the tape-vs-disk branch every old player carried. Returns a prepared
|
||
// seqread module instance (a stateful singleton — only one decoder at a time).
|
||
function openSeqread(fullPathStr) {
|
||
let sr
|
||
if (fullPathStr.startsWith('$:/TAPE') || fullPathStr.startsWith('$:\\TAPE')) {
|
||
sr = require("seqreadtape")
|
||
sr.prepare(fullPathStr)
|
||
sr.seek(0)
|
||
} else {
|
||
sr = require("seqread")
|
||
sr.prepare(fullPathStr)
|
||
}
|
||
return sr
|
||
}
|
||
|
||
// Read the 8-byte magic into a JS array (frees the scratch buffer).
|
||
function readMagic(sr) {
|
||
let p = sr.readBytes(8)
|
||
let out = []
|
||
for (let i = 0; i < 8; i++) out.push(sys.peek(p + i) & 255)
|
||
sys.free(p)
|
||
return out
|
||
}
|
||
|
||
function magicEquals(got, want) {
|
||
for (let i = 0; i < 8; i++) if (got[i] !== want[i]) return false
|
||
return true
|
||
}
|
||
|
||
// Detect container format from the 8-byte magic. Returns 'mov'|'tev'|'tav'|'tap'|'ucf'|null.
|
||
function detectFormat(magic) {
|
||
if (magicEquals(magic, MAGIC_MOV)) return 'mov'
|
||
if (magicEquals(magic, MAGIC_TEV)) return 'tev'
|
||
if (magicEquals(magic, MAGIC_TAV)) return 'tav'
|
||
if (magicEquals(magic, MAGIC_TAP)) return 'tap'
|
||
if (magicEquals(magic, MAGIC_UCF)) return 'ucf'
|
||
return null
|
||
}
|
||
|
||
// ── Luma ─────────────────────────────────────────────────────────────────────
|
||
// BT.601 integer luma from 8-bit RGB.
|
||
function luma8(r, g, b) { return (r * 77 + g * 150 + b * 29) >> 8 }
|
||
|
||
// ── Audio router ─────────────────────────────────────────────────────────────
|
||
// One playhead, deferred play(). Handles the per-packet audio codecs shared by
|
||
// the backends. TAV's bundled-MP2 (0x40) pre-decode/streaming stays in the TAV
|
||
// backend because it interleaves with the GOP display loop.
|
||
function makeAudioRouter(sr) {
|
||
const playhead = audio.getFreePlayhead(0)
|
||
const SND_BASE = audio.getBaseAddr()
|
||
const SND_MEM = audio.getMemAddr()
|
||
audio.resetParams(playhead)
|
||
audio.purgeQueue(playhead)
|
||
audio.setPcmMode(playhead)
|
||
let volume = 255
|
||
audio.setMasterVolume(playhead, volume)
|
||
|
||
let mp2Init = false
|
||
let fired = false
|
||
|
||
return {
|
||
playhead: playhead,
|
||
sndBase: SND_BASE,
|
||
sndMem: SND_MEM,
|
||
|
||
// Fire playback once, on the first displayed frame.
|
||
fire() { if (!fired) { audio.play(playhead); fired = true } },
|
||
isFired() { return fired },
|
||
|
||
stop() { audio.stop(playhead) },
|
||
resume() { audio.play(playhead) },
|
||
purge() { audio.purgeQueue(playhead); fired = false },
|
||
|
||
setVolume(v) { volume = (v < 0) ? 0 : (v > 255) ? 255 : v; audio.setMasterVolume(playhead, volume) },
|
||
getVolume() { return volume },
|
||
|
||
// MP2 packet: payload already length-known by caller; reads `len` bytes.
|
||
mp2(len) {
|
||
if (!mp2Init) { mp2Init = true; audio.mp2Init() }
|
||
sr.readBytes(len, SND_BASE - 2368)
|
||
audio.mp2Decode()
|
||
audio.mp2UploadDecoded(playhead)
|
||
},
|
||
// MP2 frame whose size is implicit in the iPF packet type.
|
||
ensureMp2() { if (!mp2Init) { mp2Init = true; audio.mp2Init() } },
|
||
|
||
// TAD packet.
|
||
tad(sampleLen, payloadLen) {
|
||
sr.readBytes(payloadLen, SND_MEM - 917504)
|
||
audio.tadDecode()
|
||
audio.tadUploadDecoded(playhead, sampleLen)
|
||
},
|
||
// Native (zstd PCMu8) packet.
|
||
nativePcm(zstdLen) {
|
||
let zstdPtr = sys.malloc(zstdLen)
|
||
sr.readBytes(zstdLen, zstdPtr)
|
||
let pcmPtr = sys.malloc(65536)
|
||
let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, pcmPtr)
|
||
if (pcmLen > 65536) { sys.free(zstdPtr); sys.free(pcmPtr); throw Error(`PCM data too long -- got ${pcmLen} bytes`) }
|
||
audio.putPcmDataByPtr(playhead, pcmPtr, pcmLen, 0)
|
||
audio.setSampleUploadLength(playhead, pcmLen)
|
||
audio.startSampleUpload(playhead)
|
||
sys.free(zstdPtr)
|
||
sys.free(pcmPtr)
|
||
},
|
||
// Raw PCM (iPF 0x1000/0x1001): payload bytes streamed directly.
|
||
rawPcm(len) {
|
||
let frame = sr.readBytes(len)
|
||
audio.putPcmDataByPtr(playhead, frame, len, 0)
|
||
audio.setSampleUploadLength(playhead, len)
|
||
audio.startSampleUpload(playhead)
|
||
sys.free(frame)
|
||
},
|
||
|
||
close() { audio.stop(playhead); audio.purgeQueue(playhead) }
|
||
}
|
||
}
|
||
|
||
// ── Subtitle engine ──────────────────────────────────────────────────────────
|
||
// Parses SSF (frame-locked 0x30) and SSF-TC (timecode 0x31) packets and exposes
|
||
// the *active* subtitle as state; the player renders it (the "postprocessor"
|
||
// stage). Font-ROM uploads are hardware writes, so the engine performs them.
|
||
// fontUploadBase: -1300607 (TEV) or -133121 (TAV) — kept per-format for parity.
|
||
function makeSubtitleEngine(sr, fontUploadBase) {
|
||
const subtitle = { visible: false, text: "", position: 0, useUnicode: false, dirty: false }
|
||
let events = []
|
||
let nextIndex = 0
|
||
let fontUploaded = false
|
||
|
||
function uploadFont(opcode, remainingBytes) {
|
||
if (remainingBytes >= 3) {
|
||
let payloadLen = sr.readShort()
|
||
if (remainingBytes >= payloadLen + 2) {
|
||
let fontData = sr.readBytes(payloadLen)
|
||
for (let i = 0; i < Math.min(payloadLen, 1920); i++) sys.poke(fontUploadBase - i, sys.peek(fontData + i))
|
||
sys.poke(-1299460, (opcode == SSF_OP_UPLOAD_LOW_FONT) ? 18 : 19)
|
||
sys.free(fontData)
|
||
}
|
||
fontUploaded = true
|
||
subtitle.useUnicode = true
|
||
}
|
||
}
|
||
|
||
return {
|
||
subtitle: subtitle,
|
||
get fontUploaded() { return fontUploaded },
|
||
|
||
// Frame-locked subtitle packet (0x30): applies immediately.
|
||
parseLegacy(packetSize) {
|
||
sr.readOneByte(); sr.readOneByte(); sr.readOneByte() // 24-bit index
|
||
let opcode = sr.readOneByte()
|
||
let remainingBytes = packetSize - 4
|
||
switch (opcode) {
|
||
case SSF_OP_SHOW: {
|
||
if (remainingBytes > 1) {
|
||
let tb = sr.readBytes(remainingBytes)
|
||
let s = ""
|
||
for (let i = 0; i < remainingBytes - 1; i++) { let b = sys.peek(tb + i); if (b === 0) break; s += String.fromCharCode(b) }
|
||
sys.free(tb)
|
||
subtitle.text = s; subtitle.visible = true; subtitle.useUnicode = fontUploaded; subtitle.dirty = true
|
||
}
|
||
break
|
||
}
|
||
case SSF_OP_HIDE: { subtitle.visible = false; subtitle.text = ""; subtitle.dirty = true; break }
|
||
case SSF_OP_MOVE: {
|
||
if (remainingBytes >= 2) {
|
||
let pos = sr.readOneByte(); sr.readOneByte()
|
||
if (pos >= 0 && pos <= 8) { subtitle.position = pos; subtitle.dirty = true }
|
||
}
|
||
break
|
||
}
|
||
case SSF_OP_UPLOAD_LOW_FONT:
|
||
case SSF_OP_UPLOAD_HIGH_FONT: { uploadFont(opcode, remainingBytes); break }
|
||
default: { if (remainingBytes > 0) { let s = sr.readBytes(remainingBytes); sys.free(s) } break }
|
||
}
|
||
},
|
||
|
||
// Timecode subtitle packet (0x31): buffered, applied by poll().
|
||
parseTC(packetSize) {
|
||
let i0 = sr.readOneByte(), i1 = sr.readOneByte(), i2 = sr.readOneByte()
|
||
let index = i0 | (i1 << 8) | (i2 << 16)
|
||
let tc = 0
|
||
for (let i = 0; i < 8; i++) { tc += sr.readOneByte() * Math.pow(2, i * 8) }
|
||
let opcode = sr.readOneByte()
|
||
let remainingBytes = packetSize - 12
|
||
let text = null
|
||
if (remainingBytes > 1 && (opcode === SSF_OP_SHOW || (opcode >= 0x10 && opcode <= 0x2F))) {
|
||
let tb = sr.readBytes(remainingBytes)
|
||
text = ""
|
||
for (let i = 0; i < remainingBytes - 1; i++) { let b = sys.peek(tb + i); if (b === 0) break; text += String.fromCharCode(b) }
|
||
sys.free(tb)
|
||
} else if (remainingBytes > 0) {
|
||
let s = sr.readBytes(remainingBytes); sys.free(s)
|
||
}
|
||
events.push({ timecode_ns: tc, index: index, opcode: opcode, text: text })
|
||
},
|
||
|
||
// Advance through timecode events whose time has been reached.
|
||
poll(currentTimeNs) {
|
||
while (nextIndex < events.length) {
|
||
let ev = events[nextIndex]
|
||
if (ev.timecode_ns > currentTimeNs) break
|
||
switch (ev.opcode) {
|
||
case SSF_OP_SHOW: subtitle.text = ev.text || ""; subtitle.visible = true; subtitle.useUnicode = fontUploaded; subtitle.dirty = true; break
|
||
case SSF_OP_HIDE: subtitle.visible = false; subtitle.text = ""; subtitle.dirty = true; break
|
||
case SSF_OP_MOVE:
|
||
if (ev.text && ev.text.length > 0) {
|
||
let pos = ev.text.charCodeAt(0)
|
||
if (pos >= 0 && pos <= 8) { subtitle.position = pos; subtitle.dirty = true }
|
||
}
|
||
break
|
||
}
|
||
nextIndex++
|
||
}
|
||
},
|
||
|
||
// After a seek: jump the event cursor to the first event at/after `tc`.
|
||
resetTo(tc) {
|
||
nextIndex = 0
|
||
for (let i = 0; i < events.length; i++) { if (events[i].timecode_ns >= tc) { nextIndex = i; break } }
|
||
subtitle.visible = false; subtitle.text = ""; subtitle.dirty = true
|
||
},
|
||
|
||
hasEvents() { return events.length > 0 }
|
||
}
|
||
}
|
||
|
||
// ── Bias lighting ────────────────────────────────────────────────────────────
|
||
// Samples the screen borders and drifts the background colour toward them —
|
||
// the "ambilight" the old players ran after each frame upload. Mode-aware
|
||
// (4/5/8 bpp) read-back, matching playtav's getRGBfromScr.
|
||
function makeBias(width, height, graphicsMode) {
|
||
const BIAS_MIN = 1.0 / 16.0
|
||
let old = [BIAS_MIN, BIAS_MIN, BIAS_MIN]
|
||
const nativeWidth = graphics.getPixelDimension()[0]
|
||
const nativeHeight = graphics.getPixelDimension()[1]
|
||
const STRIDE = 560
|
||
|
||
function rgbFromScr(x, y) {
|
||
let off = y * STRIDE + x
|
||
let fb1 = sys.peek(DISP_RG - off)
|
||
let fb2 = sys.peek(DISP_BA - off)
|
||
if (graphicsMode == 5) {
|
||
let fb3 = sys.peek(DISP_PLANE3 - off)
|
||
return [((fb1 >>> 2) & 31) / 31.0, (((fb1 & 3) << 3) | ((fb2 >>> 5) & 7)) / 31.0, (fb2 & 31) / 31.0]
|
||
} else if (graphicsMode == 4) {
|
||
return [(fb1 >>> 4) / 15.0, (fb1 & 15) / 15.0, (fb2 >>> 4) / 15.0]
|
||
} else {
|
||
let fb3 = sys.peek(DISP_PLANE3 - off)
|
||
return [fb1 / 255.0, fb2 / 255.0, fb3 / 255.0]
|
||
}
|
||
}
|
||
|
||
return function setBiasLighting() {
|
||
let samples = []
|
||
let offsetX = Math.floor((nativeWidth - width) / 2)
|
||
let offsetY = Math.floor((nativeHeight - height) / 2)
|
||
let stepX = Math.max(8, Math.floor(width / 18))
|
||
let stepY = Math.max(8, Math.floor(height / 17))
|
||
let margin = Math.min(8, Math.floor(width / 70))
|
||
|
||
for (let x = margin; x < width - margin; x += stepX) {
|
||
samples.push(rgbFromScr(x + offsetX, margin + offsetY))
|
||
samples.push(rgbFromScr(x + offsetX, height - margin - 1 + offsetY))
|
||
}
|
||
for (let y = margin; y < height - margin; y += stepY) {
|
||
samples.push(rgbFromScr(margin + offsetX, y + offsetY))
|
||
samples.push(rgbFromScr(width - margin - 1 + offsetX, y + offsetY))
|
||
}
|
||
|
||
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_MIN + (out[0] / samples.length / 2.0)
|
||
out[1] = BIAS_MIN + (out[1] / samples.length / 2.0)
|
||
out[2] = BIAS_MIN + (out[2] / samples.length / 2.0)
|
||
|
||
let bgr = (old[0] * 5 + out[0]) / 6.0
|
||
let bgg = (old[1] * 5 + out[1]) / 6.0
|
||
let bgb = (old[2] * 5 + out[2]) / 6.0
|
||
old = [bgr, bgg, bgb]
|
||
graphics.setBackground(Math.round(bgr * 255), Math.round(bgg * 255), Math.round(bgb * 255))
|
||
}
|
||
}
|
||
|
||
// ── sampleGray source ────────────────────────────────────────────────────────
|
||
// Fill an ASCII brightness buffer (dst, dstW×dstH) by nearest-sampling the GPU
|
||
// framebuffer (the shared "player framebuffer" the backend has just blit()ted
|
||
// to). Reading the screen — rather than each backend's private frame store —
|
||
// keeps one sampler for every format/kind (TAV's GOP videoBuffer is Java-heap
|
||
// and has no JS-addressable VM address, so reading it directly is impossible).
|
||
//
|
||
// Only ~dstW·dstH peeks per call, so it is cheap regardless of frame size.
|
||
// Pixel `off` is backward-addressed (DISP_RG-off / DISP_BA-off), matching how
|
||
// every decoder writes the framebuffer. `mode` selects 4/5/8-bpp unpacking
|
||
// (mirrors playtav's getRGBfromScr).
|
||
function sampleGrayScreen(width, height, dst, dstW, dstH, mode) {
|
||
for (let y = 0; y < dstH; y++) {
|
||
let sy = (y * height / dstH) | 0
|
||
let dstRow = y * dstW
|
||
for (let x = 0; x < dstW; x++) {
|
||
let sx = (x * width / dstW) | 0
|
||
let off = sy * 560 + sx
|
||
let fb1 = sys.peek(DISP_RG - off) & 255
|
||
let fb2 = sys.peek(DISP_BA - off) & 255
|
||
let r, g, b
|
||
if (mode == 5) {
|
||
r = ((fb1 >>> 2) & 31) * 255 / 31
|
||
g = (((fb1 & 3) << 3) | ((fb2 >>> 5) & 7)) * 255 / 31
|
||
b = (fb2 & 31) * 255 / 31
|
||
} else if (mode == 8) {
|
||
r = fb1; g = fb2; b = sys.peek(DISP_PLANE3 - off) & 255
|
||
} else { // mode 4
|
||
r = (fb1 >>> 4) * 17
|
||
g = (fb1 & 15) * 17
|
||
b = (fb2 >>> 4) * 17
|
||
}
|
||
dst[dstRow + x] = luma8(r | 0, g | 0, b | 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── sampleColour source ──────────────────────────────────────────────────────
|
||
// Companion to sampleGrayScreen: fill an RGB buffer (dst, length dstW·dstH·3,
|
||
// laid out R,G,B per cell) by point-sampling the GPU framebuffer at the CENTRE
|
||
// of each cell. Used by the player's colour-ASCII postprocessor — aa.mjs picks
|
||
// each glyph from brightness, this supplies the per-cell ink colour. Same
|
||
// backend-specific `mode` (4/5/8-bpp unpacking) and same cheap ~dstW·dstH peek
|
||
// count as sampleGrayScreen.
|
||
function sampleColourScreen(width, height, dst, dstW, dstH, mode) {
|
||
for (let y = 0; y < dstH; y++) {
|
||
let sy = ((y + 0.5) * height / dstH) | 0
|
||
if (sy >= height) sy = height - 1
|
||
let dstRow = y * dstW * 3
|
||
for (let x = 0; x < dstW; x++) {
|
||
let sx = ((x + 0.5) * width / dstW) | 0
|
||
if (sx >= width) sx = width - 1
|
||
let off = sy * 560 + sx
|
||
let fb1 = sys.peek(DISP_RG - off) & 255
|
||
let fb2 = sys.peek(DISP_BA - off) & 255
|
||
let r, g, b
|
||
if (mode == 5) {
|
||
r = ((fb1 >>> 2) & 31) * 255 / 31
|
||
g = (((fb1 & 3) << 3) | ((fb2 >>> 5) & 7)) * 255 / 31
|
||
b = (fb2 & 31) * 255 / 31
|
||
} else if (mode == 8) {
|
||
r = fb1; g = fb2; b = sys.peek(DISP_PLANE3 - off) & 255
|
||
} else { // mode 4
|
||
r = (fb1 >>> 4) * 17
|
||
g = (fb1 & 15) * 17
|
||
b = (fb2 >>> 4) * 17
|
||
}
|
||
let di = dstRow + x * 3
|
||
dst[di] = r | 0; dst[di + 1] = g | 0; dst[di + 2] = b | 0
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── sampleGray / sampleColour from a RAM RGB888 frame ─────────────────────────
|
||
// Companions to the *Screen samplers that read a decoded frame straight out of a
|
||
// JS-addressable RGB888 RAM buffer (3 bytes/pixel, forward-addressed) instead of
|
||
// the GPU display planes. Backends that decode into RAM (TEV / TAV) use these so
|
||
// the ASCII renderer can sample the frame WITHOUT it ever being uploaded to the
|
||
// video adapter — the whole point of the generic RAM-frame model. Same cheap
|
||
// ~dstW·dstH·3 peek count and the same nearest-sampling geometry as the *Screen
|
||
// versions (sampleGrayRGB row-aligned; sampleColourRGB at the cell centre).
|
||
function sampleGrayRGB(srcPtr, width, height, dst, dstW, dstH) {
|
||
for (let y = 0; y < dstH; y++) {
|
||
let sy = (y * height / dstH) | 0
|
||
let dstRow = y * dstW
|
||
for (let x = 0; x < dstW; x++) {
|
||
let sx = (x * width / dstW) | 0
|
||
let o = srcPtr + (sy * width + sx) * 3
|
||
let r = sys.peek(o) & 255, g = sys.peek(o + 1) & 255, b = sys.peek(o + 2) & 255
|
||
dst[dstRow + x] = luma8(r, g, b)
|
||
}
|
||
}
|
||
}
|
||
|
||
function sampleColourRGB(srcPtr, width, height, dst, dstW, dstH) {
|
||
for (let y = 0; y < dstH; y++) {
|
||
let sy = ((y + 0.5) * height / dstH) | 0
|
||
if (sy >= height) sy = height - 1
|
||
let dstRow = y * dstW * 3
|
||
for (let x = 0; x < dstW; x++) {
|
||
let sx = ((x + 0.5) * width / dstW) | 0
|
||
if (sx >= width) sx = width - 1
|
||
let o = srcPtr + (sy * width + sx) * 3
|
||
let di = dstRow + x * 3
|
||
dst[di] = sys.peek(o) & 255; dst[di + 1] = sys.peek(o + 1) & 255; dst[di + 2] = sys.peek(o + 2) & 255
|
||
}
|
||
}
|
||
}
|
||
|
||
exports = {
|
||
MAGIC_MOV, MAGIC_TEV, MAGIC_TAV, MAGIC_TAP, MAGIC_UCF,
|
||
MP2_FRAME_SIZE, QLUT,
|
||
SSF_OP_NOP, SSF_OP_SHOW, SSF_OP_HIDE, SSF_OP_MOVE,
|
||
SSF_OP_UPLOAD_LOW_FONT, SSF_OP_UPLOAD_HIGH_FONT,
|
||
DISP_RG, DISP_BA,
|
||
openSeqread, readMagic, detectFormat, magicEquals,
|
||
luma8,
|
||
makeAudioRouter, makeSubtitleEngine, makeBias,
|
||
sampleGrayScreen, sampleColourScreen,
|
||
sampleGrayRGB, sampleColourRGB
|
||
}
|