Something went wrong. Try again.
A Playstation One (PS1) emulator fully written in Jam
Something went wrong. Try again.
jamstation cue.jam
16 kB · 507 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508// SPDX-License-Identifier: MIT// Copyright (c) 2026 Raphael Amorim
// .cue text file describes how one or more .bin files map to CD-ROM// tracks.//// FILE "track.bin" BINARY// TRACK 01 MODE2/2352// INDEX 01 00:00:00// TRACK 02 AUDIO// INDEX 01 60:24:30//// Each TRACK has a mode (MODE1/2048, MODE1/2352, MODE2/2336, MODE2/2352,// AUDIO, etc.) and one or two INDEX entries (00 = pregap start, 01 =// track data start, MSF format).
const { File, exists, canonicalize } = import("std/fs");const { Vec } = import("std/collections");const { Box } = import("std/box");
pub const TRACK_MODE_UNKNOWN: u32 = 0;pub const TRACK_MODE_AUDIO: u32 = 1;pub const TRACK_MODE_MODE1_2048: u32 = 2;pub const TRACK_MODE_MODE1_2352: u32 = 3;pub const TRACK_MODE_MODE2_2336: u32 = 4;pub const TRACK_MODE_MODE2_2352: u32 = 5;
pub const MAX_FILES: u32 = 16;pub const MAX_TRACKS: u32 = 99;pub const SECTOR: u32 = 2352;
pub const CueFile = struct { file: File, sectors: u32, // LBA where this file's sector 0 lands startLba: u32, pathLo: u32, pathHi: u32,};
pub const CueTrack = struct { number: u32, mode: u32, pregap: u32, startLba: u32, endLba: u32, index0: i32, index1: i32, fileIdx: u32,};
pub const Cue = struct { files: Vec(CueFile), tracks: Vec(CueTrack), fileCount: u32, trackCount: u32, pathBuf: Vec(u8), pathBufLen: u32, pathBufCap: u32, file: File, // current character cur: u32, // directory of the .cue rootPath: Vec(u8), rootLen: u32,
// TODO: File in std will apply it's own drop so we don't need this cfn drop(self: mut Self) { var i: u32 = 0; while (i < self.fileCount) { if (self.files.ptr[i].sectors != 0) { var f: File = self.files.ptr[i].file; f.close(); self.files.ptr[i].sectors = 0; } i = i + 1; } }};
pub fn isAlpha(c: u32) bool { match (c) { 0x41..=0x5A { return true; } // A-Z 0x61..=0x7A { return true; } // a-z _ { return false; } }}
pub fn isDigit(c: u32) bool { return c >= 0x30 && c <= 0x39;}
pub fn isSpace(c: u32) bool { return c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D;}
pub fn strEqAscii(a: *mut[] u8, b: *const[] u8, n: u32) bool { var i: u32 = 0; while (i < n) { if (a[i] != b[i]) { return false; } if (a[i] == 0) { return b[i] == 0; } i = i + 1; } return true;}
pub fn parseMsf(raw: *mut[] u8) u32 { var c: *mut[] Cue = raw as *mut[] Cue; if (!isDigit(c[0].cur)) { return 0; } const m: u32 = parseNumber(raw); if (c[0].cur != 0x3A) { return 0; } c[0].cur = c[0].file.readByte() as u32 & 0xFF; const s: u32 = parseNumber(raw); if (c[0].cur != 0x3A) { return 0; }
c[0].cur = c[0].file.readByte() as u32 & 0xFF; const f: u32 = parseNumber(raw); return f + (s * 75) + (m * 4500);}
pub fn parseNumber(raw: *mut[] u8) u32 { var c: *mut[] Cue = raw as *mut[] Cue; var n: u32 = 0; while (isDigit(c[0].cur)) { n = n * 10 + (c[0].cur - 0x30); c[0].cur = c[0].file.readByte() as u32 & 0xFF; } return n;}
pub fn skipWs(raw: *mut[] u8) { var c: *mut[] Cue = raw as *mut[] Cue; while (isSpace(c[0].cur) && !c[0].file.eof()) { c[0].cur = c[0].file.readByte() as u32 & 0xFF; }}
pub fn parseKeyword(raw: *mut[] u8, buf: *mut[] u8) u32 { var c: *mut[] Cue = raw as *mut[] Cue; var n: u32 = 0; while ((isAlpha(c[0].cur) || isDigit(c[0].cur) || c[0].cur == 0x2F) && n < 15) { buf[n] = c[0].cur as u8; n = n + 1; c[0].cur = c[0].file.readByte() as u32 & 0xFF; } buf[n] = 0; return n;}
// 1 = FILE, 2 = TRACK, 3 = INDEX, 4 = REM, 5 = PREGAP, 6 = POSTGAP,// 7 = FLAGS, 8 = MODE1/2048, 9 = MODE1/2352, 10 = MODE2/2336,// 11 = MODE2/2352, 12 = AUDIO, 13 = BINARY, 0 = unknownpub fn kwId(buf: *mut[] u8) u32 { var f: []u8 = "FILE"; var t: []u8 = "TRACK"; var idx: []u8 = "INDEX"; var rem: []u8 = "REM"; var pre: []u8 = "PREGAP"; var post: []u8 = "POSTGAP"; var fl: []u8 = "FLAGS"; var m12048: []u8 = "MODE1/2048"; var m12352: []u8 = "MODE1/2352"; var m22336: []u8 = "MODE2/2336"; var m22352: []u8 = "MODE2/2352"; var au: []u8 = "AUDIO"; var bi: []u8 = "BINARY"; if (strEqAscii(buf, f.ptr, 5)) { return 1; } if (strEqAscii(buf, t.ptr, 6)) { return 2; } if (strEqAscii(buf, idx.ptr, 6)) { return 3; } if (strEqAscii(buf, rem.ptr, 4)) { return 4; } if (strEqAscii(buf, pre.ptr, 7)) { return 5; } if (strEqAscii(buf, post.ptr, 8)) { return 6; } if (strEqAscii(buf, fl.ptr, 6)) { return 7; } if (strEqAscii(buf, m12048.ptr, 11)) { return 8; } if (strEqAscii(buf, m12352.ptr, 11)) { return 9; } if (strEqAscii(buf, m22336.ptr, 11)) { return 10; } if (strEqAscii(buf, m22352.ptr, 11)) { return 11; } if (strEqAscii(buf, au.ptr, 6)) { return 12; } if (strEqAscii(buf, bi.ptr, 7)) { return 13; } return 0;}
pub fn cueReset(c: *mut[] Cue) { var i: u32 = 0; while (i < c[0].fileCount) { if (c[0].files.ptr[i].sectors != 0) { var f: File = c[0].files.ptr[i].file; f.close(); c[0].files.ptr[i].sectors = 0; } i = i + 1; } c[0].fileCount = 0; c[0].trackCount = 0; c[0].pathBufLen = 0; c[0].rootLen = 0;}
pub fn createCue() Box(Cue) { return Box(Cue).init(Cue { files: Vec(CueFile).filled( CueFile { file: File { handle: 0 as *mut[] u8 }, sectors: 0, startLba: 0, pathLo: 0, pathHi: 0 }, MAX_FILES), tracks: Vec(CueTrack).filled( CueTrack { number: 0, mode: 0, pregap: 0, startLba: 0, endLba: 0, index0: -1, index1: -1, fileIdx: 0 }, MAX_TRACKS), fileCount: 0, trackCount: 0, pathBuf: Vec(u8).filled(0, 4096), pathBufLen: 0, pathBufCap: 4096, file: File { handle: 0 as *mut[] u8 }, cur: 0, rootPath: Vec(u8).filled(0, 512), rootLen: 0, });}
pub fn cueComputeRanges(raw: *mut[] u8) { var c: *mut[] Cue = raw as *mut[] Cue; // Initial LBA is 150 (00:02:00 lead-in). var lba: u32 = 150; var fi: u32 = 0; while (fi < c[0].fileCount) { c[0].files.ptr[fi].startLba = lba; var firstTrack: u32 = MAX_TRACKS; var lastTrack: u32 = 0; var ti: u32 = 0; while (ti < c[0].trackCount) { if (c[0].tracks.ptr[ti].fileIdx == fi) { if (firstTrack == MAX_TRACKS) { firstTrack = ti; } lastTrack = ti; } ti = ti + 1; } if (firstTrack == MAX_TRACKS) { // No tracks for this file. fi = fi + 1; continue; } const fileSectors: u32 = c[0].files.ptr[fi].sectors; if (firstTrack == lastTrack) { // Single track: pregap from INDEX 01 if present, then full file. var pregap: u32 = 0; if (c[0].tracks.ptr[firstTrack].index0 != -1 && c[0].tracks.ptr[firstTrack].index1 != -1) { pregap = c[0].tracks.ptr[firstTrack].index1 as u32; } c[0].tracks.ptr[firstTrack].pregap = pregap; c[0].tracks.ptr[firstTrack].startLba = lba + pregap; c[0].tracks.ptr[firstTrack].endLba = c[0].tracks.ptr[firstTrack].startLba + fileSectors; lba = c[0].tracks.ptr[firstTrack].endLba; } else { // Multi-track: each track's start is index1 + 150; ends at next. ti = firstTrack; while (ti <= lastTrack) { c[0].tracks.ptr[ti].startLba = (c[0].tracks.ptr[ti].index1 as u32) + 150; if (ti == lastTrack) { c[0].tracks.ptr[ti].endLba = lba + fileSectors; } else { c[0].tracks.ptr[ti].endLba = (c[0].tracks.ptr[ti + 1].index1 as u32) + 150; } ti = ti + 1; } lba = c[0].tracks.ptr[lastTrack].endLba; } fi = fi + 1; }}
pub fn cueGetTrackCount(raw: *mut[] u8) u32 { var c: *mut[] Cue = raw as *mut[] Cue; return c[0].trackCount;}
pub fn cueGetTrackLba(raw: *mut[] u8, track: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; if (track == 0) { return c[0].tracks.ptr[c[0].trackCount - 1].endLba; } if (track > c[0].trackCount) { return 0xFFFFFFFF; } return c[0].tracks.ptr[track - 1].startLba;}
pub fn cueGetTrackNumber(raw: *mut[] u8, lba: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; var i: u32 = 0; while (i < c[0].trackCount) { if (lba >= c[0].tracks.ptr[i].startLba && lba < c[0].tracks.ptr[i].endLba) { return c[0].tracks.ptr[i].number; } i = i + 1; } if (c[0].trackCount > 0) { return c[0].tracks.ptr[c[0].trackCount - 1].number; } return 1;}
pub fn cueFindTrack(raw: *mut[] u8, lba: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; var i: u32 = 0; while (i < c[0].trackCount) { if (lba >= c[0].tracks.ptr[i].startLba && lba < c[0].tracks.ptr[i].endLba) { return i; } i = i + 1; } return c[0].trackCount;}
fn fillPregap(buf: *mut[] u8) { var i: u32 = 0; while (i < SECTOR) { buf[i] = 0; i = i + 1; } i = 1; while (i < 11) { buf[i] = 0xFF; i = i + 1; }}
pub fn cueRead(raw: *mut[] u8, lba: u32, buf: *mut[] u8) i32 { var c: *mut[] Cue = raw as *mut[] Cue; const ti: u32 = cueFindTrack(raw, lba); if (ti == c[0].trackCount) { // Past last track - could be pregap or far past end. if (c[0].trackCount > 0 && lba >= c[0].tracks.ptr[c[0].trackCount - 1].endLba) { return 0; } // fill zero + sync at bytes [1..10]. fillPregap(buf); return 1; } const fi: u32 = c[0].tracks.ptr[ti].fileIdx; const off: i64 = ((lba - c[0].files.ptr[fi].startLba) as i64) * (SECTOR as i64); if (off < 0) { fillPregap(buf); return 1; } var tf: File = c[0].files.ptr[fi].file; tf.seek(off, 0); const got: u64 = tf.read(buf[0..SECTOR]); if (got != SECTOR as u64) { return 0; } return 1;}
// Return TS_DATA (0) for MODE2_2352, TS_AUDIO (1) for AUDIO, TS_PREGAP (2)// for pregap, TS_FAR (3) for past-end.pub fn cueQuery(raw: *mut[] u8, lba: u32) u32 { var c: *mut[] Cue = raw as *mut[] Cue; if (c[0].trackCount > 0 && lba >= c[0].tracks.ptr[c[0].trackCount - 1].endLba) { return 3; } const ti: u32 = cueFindTrack(raw, lba); if (ti == c[0].trackCount) { return 2; } if (c[0].tracks.ptr[ti].mode == TRACK_MODE_AUDIO) { return 1; } return 0;}
pub fn cueParse(raw: *mut[] u8, path: []u8) i32 { var c: *mut[] Cue = raw as *mut[] Cue; match (File.open(path)) { Some(opened) { c[0].file = opened; } None { return 1; } } c[0].cur = c[0].file.readByte() as u32 & 0xFF; var resolved: [4096]u8 = [0; 4096]; var pi: u32 = 0; while (path[pi] != 0 && pi < 4095) { resolved[pi] = path[pi]; pi = pi + 1; } canonicalize(path, resolved.asMutPtr()); var i: u32 = 0; var lastSlash: u32 = 0; while (resolved[i] != 0) { if (resolved[i] == 0x2F || resolved[i] == 0x5C) { lastSlash = i + 1; } i = i + 1; } var ri: u32 = 0; while (ri < lastSlash) { c[0].rootPath.ptr[ri] = resolved[ri]; ri = ri + 1; } c[0].rootPath.ptr[lastSlash] = 0; c[0].rootLen = lastSlash;
skipWs(raw); var kwbuf: [16]u8 = [0; 16];
while (!c[0].file.eof()) { const kn: u32 = parseKeyword(raw, kwbuf.asMutPtr()); if (kn == 0) { break; } const kw: u32 = kwId(kwbuf.asMutPtr()); // FILE if (kw == 1) { cueParseFile(raw); // TRACK } else if (kw == 2) { cueParseTrack(raw); // INDEX } else if (kw == 3) { cueParseIndex(raw); } else { // REM / PREGAP / POSTGAP / FLAGS - skip to end of line. while (c[0].cur != 0x0A && c[0].cur != 0x0D && !c[0].file.eof()) { c[0].cur = c[0].file.readByte() as u32 & 0xFF; } } skipWs(raw); } c[0].file.close(); cueComputeRanges(raw); return 0;}
pub fn cueParseFile(raw: *mut[] u8) { var c: *mut[] Cue = raw as *mut[] Cue; skipWs(raw); if (c[0].cur != 0x22) { // expect '"' return; } c[0].cur = c[0].file.readByte() as u32 & 0xFF;
var nameBuf: [512]u8 = [0; 512]; var ni: u32 = 0; while (ni < c[0].rootLen) { nameBuf[ni] = c[0].rootPath.ptr[ni]; ni = ni + 1; } while (c[0].cur != 0x22 && !c[0].file.eof() && ni < 510) { nameBuf[ni] = c[0].cur as u8; ni = ni + 1; c[0].cur = c[0].file.readByte() as u32 & 0xFF; } nameBuf[ni] = 0; c[0].cur = c[0].file.readByte() as u32 & 0xFF; // consume closing '"'
while (isSpace(c[0].cur)) { c[0].cur = c[0].file.readByte() as u32 & 0xFF; } while (isAlpha(c[0].cur)) { c[0].cur = c[0].file.readByte() as u32 & 0xFF; }
if (c[0].fileCount >= MAX_FILES) { return; } var namePtr: *const[] u8 = nameBuf.asPtr(); var nameSlice: []u8 = namePtr[0..ni]; if (!exists(nameSlice)) { return; } match (File.open(nameSlice)) { Some(f) { const sz: u64 = f.size(); const fi: u32 = c[0].fileCount; c[0].files.ptr[fi].file = f; c[0].files.ptr[fi].sectors = (sz as u32) / SECTOR; c[0].fileCount = fi + 1; } None { return; } }}
// TRACK NN MODE -> push a CueTrack tagged with the current file.pub fn cueParseTrack(raw: *mut[] u8) { var c: *mut[] Cue = raw as *mut[] Cue; skipWs(raw); if (!isDigit(c[0].cur)) { return; } const num: u32 = parseNumber(raw); skipWs(raw); var kwbuf: [16]u8 = [0; 16]; parseKeyword(raw, kwbuf.asMutPtr()); const kw: u32 = kwId(kwbuf.asMutPtr()); var mode: u32 = TRACK_MODE_UNKNOWN; if (kw == 8) { mode = TRACK_MODE_MODE1_2048; } else if (kw == 9) { mode = TRACK_MODE_MODE1_2352; } else if (kw == 10) { mode = TRACK_MODE_MODE2_2336; } else if (kw == 11) { mode = TRACK_MODE_MODE2_2352; } else if (kw == 12) { mode = TRACK_MODE_AUDIO; } if (c[0].trackCount >= MAX_TRACKS) { return; } const ti: u32 = c[0].trackCount; c[0].tracks.ptr[ti].number = num; c[0].tracks.ptr[ti].mode = mode; c[0].tracks.ptr[ti].pregap = 0; c[0].tracks.ptr[ti].index0 = -1; c[0].tracks.ptr[ti].index1 = -1; c[0].tracks.ptr[ti].fileIdx = c[0].fileCount - 1; c[0].trackCount = ti + 1;}
// INDEX NN MM:SS:FF -> record the MSF into the most-recent track.pub fn cueParseIndex(raw: *mut[] u8) { var c: *mut[] Cue = raw as *mut[] Cue; if (c[0].trackCount == 0) { return; } skipWs(raw); if (!isDigit(c[0].cur)) { return; } const i: u32 = parseNumber(raw); skipWs(raw); const msf: u32 = parseMsf(raw); const ti: u32 = c[0].trackCount - 1; if (i == 0) { c[0].tracks.ptr[ti].index0 = msf as i32; } if (i == 1) { c[0].tracks.ptr[ti].index1 = msf as i32; }}