Something went wrong. Try again.
A Playstation One (PS1) emulator fully written in Jam
Something went wrong. Try again.
jamstation xa.jam
5.9 kB · 152 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153// SPDX-License-Identifier: MIT// Copyright (c) 2026 Raphael Amorim
// note:// XA sectors are Mode 2 / Form 2 CD-ROM sectors carrying compressed// 4-bit ADPCM audio. The format is://// bytes 0..11 : sync (00 FF FF FF ... FF 00)// bytes 12..15 : address (MM:SS:FF:mode)// bytes 16..23 : sub-header (file/channel/sub-mode/coding-info), x2// bytes 24..2351: 18 sound groups, each 128 bytes:// 4 sound-parameter bytes (shift|filter) + 4 mirror// + 112 nibble bytes (28 nibbles × 4 columns)//// Each sound group's 4 columns are interpreted as 4 blocks: for stereo// the columns alternate L/R/L/R; for mono all 4 are sequential mono.// Each block produces 28 i16 samples via:// s = (nibble << shift) + (f0 * prev1 + f1 * prev2 + 32) / 64// clamped to int16.
// ADPCM predictor coefficient tablespub fn xaPosCoef(idx: u32) i32 { if (idx == 0) { return 0; } if (idx == 1) { return 60; } if (idx == 2) { return 115; } if (idx == 3) { return 98; } return 122;}
pub fn xaNegCoef(idx: u32) i32 { if (idx == 0) { return 0; } if (idx == 1) { return 0; } if (idx == 2) { return -52; } if (idx == 3) { return -55; } return -60;}
pub fn xaWrap16(v: i32) i32 { return ((v & 0xFFFF) ^ 0x8000) - 0x8000;}
// Sub-mode byte at sector offset 0x12: bit 2 set = real-time audio.pub fn xaSectorIsAudio(secBuf: *mut[] u8) bool { return (secBuf[0x12] & 0x04) != 0;}
// File/channel match per CDL_SETFILTER. The sub-header (offsets// 0x10..0x11) carries (file_number, channel_number, sub_mode, coding).pub fn xaSectorMatchesFilter(secBuf: *mut[] u8, xaFile: u32, xaChannel: u32) bool { return ((secBuf[0x10] as u32) == xaFile) && ((secBuf[0x11] as u32) == xaChannel);}
// Stereo flag from coding-info byte at offset 0x13 (bit 0 = stereo).pub fn xaSectorIsStereo(secBuf: *mut[] u8) bool { return (secBuf[0x13] & 0x01) != 0;}
// Sampling frequency: bit 2 = 18.9 kHz (1) vs 37.8 kHz (0).pub fn xaSectorIs18kHz(secBuf: *mut[] u8) bool { return ((secBuf[0x13] >> 2) & 1) != 0;}
// Decode one 28-sample block. `idx` is the byte offset to the start of// the 128-byte sound group inside the sector buffer (idx walks 24,// 152, 280, ... in steps of 128). `blk` is the block index within the// group (0..3).// `nib` is the nibble half (0 = low, 1 = high). `dst` receives 28 i16// samples; `hist` is a 2-entry signed-16 history (hist[0] = previous// sample, hist[1] = sample-before-previous).pub fn xaDecodeBlock(secBuf: *mut[] u8, idx: u32, blk: u32, nib: u32, dst: *mut[] i32, hist: *mut[] i32) { const hdr: u32 = secBuf[idx + 4 + blk * 2 + nib] as u32; // Reserved ADPCM shift values 13..15 act as 9 on hardware (DuckStation // cdrom.cpp:285-289). A naive `12 - (hdr&0xF)` would leave shift negative // and the `old << shift` below would then be an undefined negative shift; // clamp. var shiftVal: i32 = (hdr & 0x0F) as i32; if (shiftVal > 12) { shiftVal = 9; } const shift: i32 = 12 - shiftVal; // self note: mask to 0x30 then >>4 const filter: u32 = (hdr >> 4) & 3; const f0: i32 = xaPosCoef(filter); const f1: i32 = xaNegCoef(filter); var j: u32 = 0; while (j < 28) { const raw: u32 = (secBuf[idx + 16 + blk + j * 4] as u32); const n: u32 = (raw >> (nib * 4)) & 0x0F; // Sign-extend the 4-bit nibble to 16 bits. var t: i32 = n as i32; if ((n & 0x08) != 0) { t = t - 16; } const old: i32 = (t << 12) >> 12; const shifted: i32 = old << shift; const pred: i32 = (f0 * hist[0] + f1 * hist[1] + 32) / 64; // self note: wrap to int16 here, do not saturate const s: i32 = xaWrap16(shifted + pred); hist[1] = hist[0]; hist[0] = s; dst[j] = s; j = j + 1; }}
// for stereo sectors writes 18 * 4/2 = 4032// samples total split across leftBuf / rightBuf (2016 each); for mono// writes 4032 samples into monoBuf. Returns the number of samples// emitted to ONE channel (4032 mono, 2016 per side for stereo).pub fn xaDecodeSector(secBuf: *mut[] u8, leftBuf: *mut[] i32, rightBuf: *mut[] i32, monoBuf: *mut[] i32, leftH: *mut[] i32, rightH: *mut[] i32) u32 { const stereo: bool = xaSectorIsStereo(secBuf); // first sound group starts at byte 24 var src: u32 = 24; var leftIdx: u32 = 0; var rightIdx: u32 = 0; var monoIdx: u32 = 0; var i: u32 = 0; // 28 = samples per ADPCM block. Stack-allocated; rewritten by // every xaDecodeBlock call below. var blockBuf: [28]i32 = [0; 28]; while (i < 18) { var blk: u32 = 0; while (blk < 4) { if (stereo) { xaDecodeBlock(secBuf, src, blk, 0, blockBuf.asMutPtr(), leftH); var k: u32 = 0; while (k < 28) { leftBuf[leftIdx + k] = blockBuf[k]; k = k + 1; } leftIdx = leftIdx + 28; xaDecodeBlock(secBuf, src, blk, 1, blockBuf.asMutPtr(), rightH); k = 0; while (k < 28) { rightBuf[rightIdx + k] = blockBuf[k]; k = k + 1; } rightIdx = rightIdx + 28; } else { xaDecodeBlock(secBuf, src, blk, 0, blockBuf.asMutPtr(), leftH); var k: u32 = 0; while (k < 28) { monoBuf[monoIdx + k] = blockBuf[k]; k = k + 1; } monoIdx = monoIdx + 28; xaDecodeBlock(secBuf, src, blk, 1, blockBuf.asMutPtr(), leftH); k = 0; while (k < 28) { monoBuf[monoIdx + k] = blockBuf[k]; k = k + 1; } monoIdx = monoIdx + 28; } blk = blk + 1; } src = src + 128; i = i + 1; } if (stereo) { return leftIdx; } return monoIdx;}