diff --git a/crates/oasis-backend-psp/Cargo.lock b/crates/oasis-backend-psp/Cargo.lock index 723edab..622b9e6 100644 --- a/crates/oasis-backend-psp/Cargo.lock +++ b/crates/oasis-backend-psp/Cargo.lock @@ -748,7 +748,7 @@ dependencies = [ [[package]] name = "psp" version = "0.4.0" -source = "git+https://github.com/AndrewAltimit/rust-psp#edb63cfb7828374fffac218138187b52d0d567f5" +source = "git+https://github.com/AndrewAltimit/rust-psp#680d3d3284ace336219daf05410ad258daa675bf" dependencies = [ "bitflags", "libm", diff --git a/crates/oasis-backend-psp/Cargo.toml b/crates/oasis-backend-psp/Cargo.toml index 09d4f77..8f9a6f2 100644 --- a/crates/oasis-backend-psp/Cargo.toml +++ b/crates/oasis-backend-psp/Cargo.toml @@ -14,9 +14,11 @@ repository = "https://github.com/AndrewAltimit/oasis-os" authors = ["AndrewAltimit"] [features] -default = ["kernel-mode"] +default = [] +# kernel-mode bundles all kernel sub-features EXCEPT kernel-exception +# (intentionally excluded: broken on PSP-3000 + 6.20 PRO-C). kernel-mode = ["psp/kernel", "kernel-volatile", "kernel-me-clock", "kernel-me"] -# Granular kernel sub-features: +# Granular kernel sub-features (each enables psp/kernel independently): kernel-exception = ["psp/kernel"] # sceKernelRegisterDefaultExceptionHandler (broken on PSP-3000 + 6.20 PRO-C) kernel-volatile = ["psp/kernel"] # sceKernelVolatileMemTryLock (extra 4MB) kernel-me-clock = ["psp/kernel"] # scePowerGetMeClockFrequency diff --git a/crates/oasis-backend-psp/src/audio.rs b/crates/oasis-backend-psp/src/audio.rs index 05353b0..a32d4aa 100644 --- a/crates/oasis-backend-psp/src/audio.rs +++ b/crates/oasis-backend-psp/src/audio.rs @@ -1,9 +1,16 @@ -//! Audio playback (MP3 via psp::mp3 + psp::audio) and `AudioBackend` trait. - -use std::sync::Arc; +//! Audio playback (MP3 via sceAudiocodec + psp::audio) and `AudioBackend` trait. +//! +//! Uses the low-level `sceAudiocodec` frame-by-frame decoder instead of the +//! high-level `sceMp3*` API, which crashes on real PSP hardware after ~2-3 +//! handle reuse cycles. +//! +//! MP3 data is **streamed from file** using a small fixed-size read buffer +//! (32 KB) to avoid large heap allocations that cause heap fragmentation +//! and crashes on PSP's limited 24 MB user memory. use psp::audio::{AudioChannel, AudioFormat}; -use psp::mp3::Mp3Decoder; +use psp::audiocodec::{AudiocodecDecoder, CodecType}; +use psp::mp3::{find_sync, skip_id3v2}; use oasis_core::backend::{AudioBackend, AudioTrackId}; use oasis_core::error::{OasisError, Result}; @@ -13,26 +20,143 @@ use crate::threading::{AudioCmd, AudioHandle, send_audio_cmd}; /// Standard MP3 frame size (MPEG1 Layer 3). const MP3_FRAME_SAMPLES: i32 = 1152; -/// MP3 playback engine using the PSP's hardware MP3 decoder. +/// Size of the read buffer for streaming MP3 from file. +/// 32 KB is enough for many MP3 frames and avoids large heap allocations. +const READ_BUF_SIZE: usize = 32 * 1024; + +/// Load AV codec modules once (idempotent). Called lazily on first play +/// to avoid conflicts with the PRX overlay at boot time. +fn load_av_modules_once() { + use core::sync::atomic::{AtomicBool, Ordering}; + static LOADED: AtomicBool = AtomicBool::new(false); + if LOADED.swap(true, Ordering::Relaxed) { + return; // Already loaded. + } + unsafe { + psp::sys::sceUtilityLoadModule(psp::sys::Module::AvCodec); + psp::sys::sceUtilityLoadModule(psp::sys::Module::AvMpegBase); + psp::sys::sceUtilityLoadModule(psp::sys::Module::AvMp3); + } +} + +// --------------------------------------------------------------------------- +// MP3 frame header parsing +// --------------------------------------------------------------------------- + +/// MPEG version bitrate tables (kbps). Index: bitrate_index (1..14). +const BITRATES_V1_L3: [u32; 15] = [ + 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, +]; +const BITRATES_V2_L3: [u32; 15] = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; + +/// Sample rates by MPEG version. [version_index][srate_index] +const SAMPLE_RATES: [[u32; 3]; 4] = [ + [11025, 12000, 8000], // MPEG 2.5 + [0, 0, 0], // reserved + [22050, 24000, 16000], // MPEG 2 + [44100, 48000, 32000], // MPEG 1 +]; + +/// Parsed MP3 frame header. +struct Mp3FrameHeader { + /// Sample rate in Hz. + sample_rate: u32, + /// Bitrate in kbps. + bitrate: u32, + /// Number of channels (1 or 2). + channels: u32, +} + +/// Parse an MP3 frame header from 4 bytes starting at a sync position. +fn parse_mp3_header(data: &[u8]) -> Option { + if data.len() < 4 { + return None; + } + let b1 = data[1]; + let b2 = data[2]; + let b3 = data[3]; + + let version_bits = (b1 >> 3) & 0x03; + let layer_bits = (b1 >> 1) & 0x03; + let bitrate_idx = (b2 >> 4) & 0x0F; + let srate_idx = (b2 >> 2) & 0x03; + let channel_mode = (b3 >> 6) & 0x03; + + if version_bits == 1 + || layer_bits == 0 + || bitrate_idx == 0 + || bitrate_idx == 15 + || srate_idx == 3 + { + return None; + } + // Only Layer III. + if layer_bits != 1 { + return None; + } + + let is_v1 = version_bits == 3; + let bitrate = if is_v1 { + BITRATES_V1_L3[bitrate_idx as usize] + } else { + BITRATES_V2_L3[bitrate_idx as usize] + }; + let sample_rate = SAMPLE_RATES[version_bits as usize][srate_idx as usize]; + if sample_rate == 0 || bitrate == 0 { + return None; + } + let channels = if channel_mode == 3 { 1 } else { 2 }; + + Some(Mp3FrameHeader { + sample_rate, + bitrate, + channels, + }) +} + +// --------------------------------------------------------------------------- +// AudioPlayer — streaming sceAudiocodec-based MP3 playback +// --------------------------------------------------------------------------- + +/// MP3 playback engine using sceAudiocodec with file streaming. /// -/// Uses RAII wrappers from `psp::mp3::Mp3Decoder` and -/// `psp::audio::AudioChannel`. Call `load_and_play()` to start, -/// `update()` each frame to pump decoded audio, and `stop()` to halt. +/// Instead of loading the entire MP3 into a Vec (which fragments the PSP's +/// 24MB heap after 2-3 songs), this streams from an open file descriptor +/// using a fixed 32KB read buffer — matching the PRX plugin's approach. +/// +/// The `AudiocodecDecoder` and `AudioChannel` are created once and reused +/// across all songs. pub struct AudioPlayer { - decoder: Option, + decoder: Option, channel: Option, + /// Fixed read buffer for streaming MP3 data from file. + read_buf: Vec, + /// Number of valid bytes in `read_buf`. + buf_valid: usize, + /// Current read position within `read_buf`. + buf_pos: usize, + /// Open file descriptor for the current song (negative = none). + fd: psp::sys::SceUid, + /// Total file size in bytes. + file_size: usize, + /// Bytes read from file so far. + file_pos: usize, playing: bool, paused: bool, - /// Hardware volume (0x0000..=0x8000 range for PSP audio API). + /// Hardware volume (0x0000..=0x8000). hw_volume: i32, - /// Cached MP3 info. + /// PCM decode buffer (stereo: 1152 * 2 samples). + pcm_buf: Vec, + /// Cached MP3 info from first frame header. pub sample_rate: u32, pub bitrate: u32, pub channels: u32, /// Count of decoded MP3 frames (for position tracking). pub frames_decoded: u32, - /// Total file size in bytes (for duration estimation). + /// Total MP3 data size in bytes (for duration estimation). pub data_size: u32, + /// Consecutive decode errors (to bail on corrupt data). + error_count: u32, } impl AudioPlayer { @@ -40,114 +164,314 @@ impl AudioPlayer { Self { decoder: None, channel: None, + read_buf: vec![0u8; READ_BUF_SIZE], + buf_valid: 0, + buf_pos: 0, + fd: psp::sys::SceUid(-1), + file_size: 0, + file_pos: 0, playing: false, paused: false, hw_volume: 0x8000, + pcm_buf: vec![0i16; 1152 * 2], sample_rate: 0, bitrate: 0, channels: 0, frames_decoded: 0, data_size: 0, + error_count: 0, } } - /// Initialize the audio subsystem. - /// - /// With the new SDK, `Mp3Decoder` handles resource init on construction, - /// so this is a no-op kept for API compatibility with the worker thread. + /// Initialize the audio subsystem (no-op, kept for API compat). pub fn init(&mut self) -> bool { true } - /// Load an MP3 file from the Memory Stick and start playback. - pub fn load_and_play(&mut self, path: &str) -> bool { - let data = match psp::io::read_to_vec(path) { - Ok(d) => d, - Err(_) => return false, - }; - self.load_and_play_data(&data) + /// Close the current file if open. + fn close_file(&mut self) { + if self.fd >= psp::sys::SceUid(0) { + unsafe { psp::sys::sceIoClose(self.fd) }; + self.fd = psp::sys::SceUid(-1); + } } - /// Start playback from raw MP3 data already in memory. - pub fn load_and_play_data(&mut self, data: &[u8]) -> bool { - self.stop(); + /// Load an MP3 file from the Memory Stick and start streaming playback. + pub fn load_and_play(&mut self, path: &str) -> bool { + self.playing = false; + self.paused = false; + self.close_file(); - if data.is_empty() { + load_av_modules_once(); + + // Open file. + let mut path_bytes: Vec = path.as_bytes().to_vec(); + path_bytes.push(0); // null-terminate + let fd = + unsafe { psp::sys::sceIoOpen(path_bytes.as_ptr(), psp::sys::IoOpenFlags::RD_ONLY, 0) }; + if fd < psp::sys::SceUid(0) { return false; } - let decoder = match Mp3Decoder::new(data) { - Ok(d) => d, - Err(e) => { - psp::dprintln!("OASIS_OS: Mp3Decoder failed: {:?}", e); - return false; - }, + // Get file size. + let size = unsafe { psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::End) } as usize; + unsafe { psp::sys::sceIoLseek(fd, 0, psp::sys::IoWhence::Set) }; + if size == 0 { + unsafe { psp::sys::sceIoClose(fd) }; + return false; + } + + self.fd = fd; + self.file_size = size; + self.file_pos = 0; + self.data_size = size as u32; + + // Initial read into buffer. + let read = unsafe { + psp::sys::sceIoRead( + self.fd, + self.read_buf.as_mut_ptr() as *mut _, + READ_BUF_SIZE as u32, + ) }; + if read <= 0 { + self.close_file(); + return false; + } + self.buf_valid = read as usize; + self.buf_pos = 0; + self.file_pos = self.buf_valid; + + // Skip ID3v2 tag. If the tag is larger than the read buffer + // (common with embedded album art), seek past it in the file + // and re-read from the first audio frame. + let id3_skip = skip_id3v2(&self.read_buf[..self.buf_valid]); + if id3_skip > 0 { + // Adjust data_size to exclude the tag for duration estimation. + self.data_size = self.data_size.saturating_sub(id3_skip as u32); + if id3_skip < self.buf_valid { + self.buf_pos = id3_skip; + } else { + // Tag exceeds buffer — seek past it in the file. + unsafe { + psp::sys::sceIoLseek(self.fd, id3_skip as i64, psp::sys::IoWhence::Set); + } + self.file_pos = id3_skip; + let re_read = unsafe { + psp::sys::sceIoRead( + self.fd, + self.read_buf.as_mut_ptr() as *mut _, + READ_BUF_SIZE as u32, + ) + }; + if re_read <= 0 { + self.close_file(); + return false; + } + self.buf_valid = re_read as usize; + self.buf_pos = 0; + self.file_pos = id3_skip + self.buf_valid; + } + } - self.sample_rate = decoder.sample_rate(); - self.bitrate = decoder.bitrate(); - self.channels = decoder.channels() as u32; - self.frames_decoded = 0; - self.data_size = data.len() as u32; + // Parse first frame header for metadata. + let buf_slice = &self.read_buf[self.buf_pos..self.buf_valid]; + if let Some(sync_off) = find_sync(buf_slice, 0) { + if let Some(hdr) = parse_mp3_header(&buf_slice[sync_off..]) { + self.sample_rate = hdr.sample_rate; + self.bitrate = hdr.bitrate; + self.channels = hdr.channels; + } + } - let fmt = if self.channels == 1 { - AudioFormat::Mono - } else { - AudioFormat::Stereo - }; + self.frames_decoded = 0; + self.error_count = 0; + + // Create the audiocodec decoder once, reuse forever. + if self.decoder.is_none() { + match AudiocodecDecoder::new(CodecType::Mp3) { + Ok(dec) => self.decoder = Some(dec), + Err(_) => { + self.close_file(); + return false; + }, + } + } - let channel = match AudioChannel::reserve(MP3_FRAME_SAMPLES, fmt) { - Ok(ch) => ch, - Err(e) => { - psp::dprintln!("OASIS_OS: AudioChannel::reserve failed: {:?}", e,); - return false; - }, - }; + // Reuse audio channel if we already have one. + if self.channel.is_none() { + match AudioChannel::reserve(MP3_FRAME_SAMPLES, AudioFormat::Stereo) { + Ok(ch) => self.channel = Some(ch), + Err(_) => { + self.close_file(); + return false; + }, + } + } - self.decoder = Some(decoder); - self.channel = Some(channel); self.playing = true; self.paused = false; - - psp::dprintln!( - "OASIS_OS: MP3 loaded - {}Hz, {}kbps, {}ch", - self.sample_rate, - self.bitrate, - self.channels, - ); true } + /// Start playback from in-memory MP3 data. + /// + /// Writes data to a temp file and streams from it, keeping the same + /// streaming architecture. Falls back to the file path approach. + pub fn load_and_play_owned(&mut self, data: Vec) -> bool { + // Write data to a temp file, then stream from it. + // This avoids keeping the full MP3 in heap memory. + let temp_path = "ms0:/PSP/GAME/oasis_os/__temp_audio.mp3"; + let temp_path_c = b"ms0:/PSP/GAME/oasis_os/__temp_audio.mp3\0"; + + let fd = unsafe { + psp::sys::sceIoOpen( + temp_path_c.as_ptr(), + psp::sys::IoOpenFlags::WR_ONLY + | psp::sys::IoOpenFlags::CREAT + | psp::sys::IoOpenFlags::TRUNC, + 0o777, + ) + }; + if fd < psp::sys::SceUid(0) { + return false; + } + let written = unsafe { psp::sys::sceIoWrite(fd, data.as_ptr() as *const _, data.len()) }; + unsafe { psp::sys::sceIoClose(fd) }; + + // Free the input data immediately — we don't need it anymore. + drop(data); + + if written <= 0 { + return false; + } + + self.load_and_play(temp_path) + } + + /// Start playback from a borrowed slice (copies data internally). + pub fn load_and_play_data(&mut self, data: &[u8]) -> bool { + self.load_and_play_owned(data.to_vec()) + } + + /// Refill the read buffer: compact consumed data and read more from file. + /// + /// Uses the PRX plugin's compact+stream-refill pattern: when more than + /// half the buffer is consumed, shift remaining data to the front and + /// top up in small chunks (4KB) to avoid blocking audio output. + fn refill_buffer(&mut self) { + // Compact when more than half consumed. + if self.buf_pos > READ_BUF_SIZE / 2 && self.file_pos < self.file_size { + let remaining = self.buf_valid - self.buf_pos; + if remaining > 0 { + // SAFETY: Manual byte-by-byte copy (LLVM memcpy can recurse + // on MIPS). src and dst overlap so we copy forward. + let ptr = self.read_buf.as_mut_ptr(); + for i in 0..remaining { + unsafe { *ptr.add(i) = *ptr.add(self.buf_pos + i) }; + } + } + self.buf_valid = remaining; + self.buf_pos = 0; + } + + // Top up buffer if there's room (small reads to avoid audio stalls). + if self.buf_valid < READ_BUF_SIZE && self.file_pos < self.file_size { + let room = READ_BUF_SIZE - self.buf_valid; + let chunk = room.min(4096); + let read = unsafe { + psp::sys::sceIoRead( + self.fd, + self.read_buf.as_mut_ptr().add(self.buf_valid) as *mut _, + chunk as u32, + ) + }; + if read > 0 { + self.buf_valid += read as usize; + self.file_pos += read as usize; + } + } + } + /// Pump decoded audio to the output channel. Call each frame. pub fn update(&mut self) { if !self.playing || self.paused { return; } - let (Some(decoder), Some(channel)) = (&mut self.decoder, &self.channel) else { + if self.decoder.is_none() || self.channel.is_none() { return; + } + + // Refill buffer from file (before borrowing decoder/channel). + self.refill_buffer(); + + let avail = self.buf_valid - self.buf_pos; + if avail < 4 { + self.playing = false; + self.close_file(); + return; + } + + // Find next MP3 frame sync in the buffer. + let sync_pos = match find_sync(&self.read_buf[..self.buf_valid], self.buf_pos) { + Some(p) => p, + None => { + self.buf_pos = self.buf_valid; + return; + }, }; + self.buf_pos = sync_pos; - match decoder.decode_frame() { - Ok(samples) if !samples.is_empty() => { + if self.buf_valid - self.buf_pos < 8 { + return; + } + + // Decode one frame via sceAudiocodec. + // We use split borrows: decoder, channel, read_buf, and pcm_buf are + // all separate fields, so borrowck allows simultaneous access. + for s in &mut self.pcm_buf { + *s = 0; + } + + let decoder = self.decoder.as_mut().unwrap(); + let buf_pos = self.buf_pos; + let buf_valid = self.buf_valid; + let result = decoder.decode(&self.read_buf[buf_pos..buf_valid], &mut self.pcm_buf); + + match result { + Ok(consumed) => { + if consumed == 0 { + self.error_count += 1; + self.buf_pos += 1; + if self.error_count > 100 { + self.playing = false; + self.close_file(); + } + return; + } + self.error_count = 0; + self.buf_pos += consumed; self.frames_decoded += 1; - // output_blocking paces playback to hardware timing. - let _ = channel.output_blocking(self.hw_volume, samples); + let channel = self.channel.as_ref().unwrap(); + let _ = channel.output_blocking(self.hw_volume, &self.pcm_buf); }, - _ => { - // End of stream or decode error. - self.playing = false; + Err(_) => { + self.error_count += 1; + self.buf_pos += 1; + if self.error_count > 100 { + self.playing = false; + self.close_file(); + } }, } } - /// Stop playback and release resources. + /// Stop playback. Decoder and channel are kept alive for reuse. pub fn stop(&mut self) { - // Drop order: channel first (stops hardware output), then decoder. - self.channel = None; - self.decoder = None; self.playing = false; self.paused = false; + self.close_file(); } /// Toggle pause/resume. @@ -182,23 +506,28 @@ impl AudioPlayer { if self.bitrate == 0 { return 0; } - // bitrate is in kbps, data_size in bytes. (self.data_size as u64 * 8) / self.bitrate as u64 } } +impl Drop for AudioPlayer { + fn drop(&mut self) { + self.close_file(); + } +} + // --------------------------------------------------------------------------- // AudioBackend trait implementation (delegates to worker thread) // --------------------------------------------------------------------------- /// PSP audio backend that delegates to the audio worker thread. /// -/// Stores loaded track data locally and sends it to the audio thread -/// on `play()`. Reads playback state from the shared `SpinMutex` via -/// `AudioHandle`. +/// Track data is moved (not cloned) to the audio thread on play to +/// minimize peak memory. Only one track's data lives in this struct +/// at a time — previous tracks are freed on load. pub struct PspAudioBackend { audio: AudioHandle, - tracks: Vec>>>, + tracks: Vec>>, current_track: Option, volume: u8, } @@ -222,7 +551,11 @@ impl AudioBackend for PspAudioBackend { fn load_track(&mut self, data: &[u8]) -> Result { let id = self.tracks.len() as u64; - self.tracks.push(Some(Arc::new(data.to_vec()))); + // Free all previous track data to conserve PSP memory. + for slot in &mut self.tracks { + *slot = None; + } + self.tracks.push(Some(data.to_vec())); Ok(AudioTrackId(id)) } @@ -230,12 +563,10 @@ impl AudioBackend for PspAudioBackend { let idx = track.0 as usize; let data = self .tracks - .get(idx) - .and_then(|slot| slot.as_ref()) + .get_mut(idx) + .and_then(|slot| slot.take()) .ok_or_else(|| OasisError::Backend(format!("track {} not loaded", track.0)))?; - // Arc::clone is cheap (ref count bump) -- avoids duplicating the - // entire MP3 buffer on a memory-constrained device. - send_audio_cmd(AudioCmd::LoadAndPlayData(Arc::clone(data))); + send_audio_cmd(AudioCmd::LoadAndPlayData(data)); self.current_track = Some(track.0); Ok(()) } @@ -270,11 +601,11 @@ impl AudioBackend for PspAudioBackend { } fn position_ms(&self) -> u64 { - self.audio.state().position_ms + self.audio.position_ms() } fn duration_ms(&self) -> u64 { - self.audio.state().duration_ms + self.audio.duration_ms() } fn unload_track(&mut self, track: AudioTrackId) -> Result<()> { diff --git a/crates/oasis-backend-psp/src/main.rs b/crates/oasis-backend-psp/src/main.rs index 2d9c4d6..8c3f009 100644 --- a/crates/oasis-backend-psp/src/main.rs +++ b/crates/oasis-backend-psp/src/main.rs @@ -578,6 +578,11 @@ fn psp_main() { let mut mp_loaded = false; let mut mp_file_name = String::new(); + // AV codec modules (AvCodec, AvMpegBase, AvMp3) are loaded lazily + // by the audio thread on first play. Loading them here at startup + // would conflict with the PRX overlay's sceAudiocodec if the PRX + // initialized before the EBOOT was launched. + // Single background worker thread handles both audio and file I/O. let (audio, io) = oasis_backend_psp::spawn_workers(); let mut pv_loading = false; // true while waiting for async texture load diff --git a/crates/oasis-backend-psp/src/threading.rs b/crates/oasis-backend-psp/src/threading.rs index 2d75425..46b1eeb 100644 --- a/crates/oasis-backend-psp/src/threading.rs +++ b/crates/oasis-backend-psp/src/threading.rs @@ -2,11 +2,12 @@ //! //! Uses `psp::thread::ThreadBuilder` for native PSP kernel threads with //! priority tuning. Communication uses lock-free `SpscQueue` for commands -//! and `SpinMutex` for shared state readable from the main thread. +//! and bare atomics for shared state (lock-free to avoid priority inversion +//! on single-core PSP where a high-priority audio thread could starve the +//! main thread if both contend on a spinlock). -use std::sync::Arc; - -use psp::sync::{SpinMutex, SpscQueue}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use psp::sync::SpscQueue; use psp::thread::ThreadBuilder; use crate::audio::AudioPlayer; @@ -25,45 +26,17 @@ static IO_CMD_QUEUE: SpscQueue = SpscQueue::new(); static IO_RESP_QUEUE: SpscQueue = SpscQueue::new(); // --------------------------------------------------------------------------- -// Shared audio state (SpinMutex for richer state than bare atomics) +// Shared audio state (lock-free atomics -- no priority inversion) // --------------------------------------------------------------------------- -/// Shared audio state protected by a spinlock. -/// -/// Readable from the main thread and written by the audio thread. -/// PSP is single-core so SpinMutex has near-zero overhead for short -/// critical sections. -static SHARED_AUDIO: SpinMutex = SpinMutex::new(SharedAudioState::new()); - -/// Audio state shared between the audio thread and main thread. -#[derive(Clone)] -pub struct SharedAudioState { - pub playing: bool, - pub paused: bool, - pub sample_rate: u32, - pub bitrate: u32, - pub channels: u32, - pub position_ms: u64, - pub duration_ms: u64, - pub track_name: [u8; 64], - pub track_name_len: usize, -} - -impl SharedAudioState { - const fn new() -> Self { - Self { - playing: false, - paused: false, - sample_rate: 0, - bitrate: 0, - channels: 0, - position_ms: 0, - duration_ms: 0, - track_name: [0u8; 64], - track_name_len: 0, - } - } -} +static AUDIO_PLAYING: AtomicBool = AtomicBool::new(false); +static AUDIO_PAUSED: AtomicBool = AtomicBool::new(false); +static AUDIO_SAMPLE_RATE: AtomicU32 = AtomicU32::new(0); +static AUDIO_BITRATE: AtomicU32 = AtomicU32::new(0); +static AUDIO_CHANNELS: AtomicU32 = AtomicU32::new(0); +// Position/duration stored as u32 milliseconds (max ~49 days, plenty). +static AUDIO_POSITION_MS: AtomicU32 = AtomicU32::new(0); +static AUDIO_DURATION_MS: AtomicU32 = AtomicU32::new(0); // --------------------------------------------------------------------------- // Audio commands @@ -72,7 +45,7 @@ impl SharedAudioState { /// Commands for the dedicated audio thread. pub enum AudioCmd { LoadAndPlay(String), - LoadAndPlayData(Arc>), + LoadAndPlayData(Vec), Pause, Resume, Stop, @@ -81,7 +54,7 @@ pub enum AudioCmd { Shutdown, } -/// Handle to the background audio thread (reads from SHARED_AUDIO). +/// Handle to the background audio thread (reads shared atomics). pub struct AudioHandle; impl AudioHandle { @@ -90,37 +63,32 @@ impl AudioHandle { let _ = AUDIO_QUEUE.push(cmd); } - /// Snapshot the current audio state (short spinlock hold). - pub fn state(&self) -> SharedAudioState { - SHARED_AUDIO.lock().clone() - } - pub fn is_playing(&self) -> bool { - SHARED_AUDIO.lock().playing + AUDIO_PLAYING.load(Ordering::Relaxed) } pub fn is_paused(&self) -> bool { - SHARED_AUDIO.lock().paused + AUDIO_PAUSED.load(Ordering::Relaxed) } pub fn sample_rate(&self) -> u32 { - SHARED_AUDIO.lock().sample_rate + AUDIO_SAMPLE_RATE.load(Ordering::Relaxed) } pub fn bitrate(&self) -> u32 { - SHARED_AUDIO.lock().bitrate + AUDIO_BITRATE.load(Ordering::Relaxed) } pub fn channels(&self) -> u32 { - SHARED_AUDIO.lock().channels + AUDIO_CHANNELS.load(Ordering::Relaxed) } pub fn position_ms(&self) -> u64 { - SHARED_AUDIO.lock().position_ms + AUDIO_POSITION_MS.load(Ordering::Relaxed) as u64 } pub fn duration_ms(&self) -> u64 { - SHARED_AUDIO.lock().duration_ms + AUDIO_DURATION_MS.load(Ordering::Relaxed) as u64 } } @@ -194,28 +162,31 @@ impl IoHandle { /// Spawn the background audio and I/O threads. /// -/// Returns handles for audio state and I/O responses. +/// Returns handles for audio state and I/O responses. The `JoinHandle`s +/// are leaked intentionally — the worker threads run for the lifetime of +/// the process, and dropping a `JoinHandle` terminates its thread. pub fn spawn_workers() -> (AudioHandle, IoHandle) { // Audio thread: high priority (16) for low-latency playback. - let audio_result = ThreadBuilder::new(b"oasis_audio\0") + if let Ok(handle) = ThreadBuilder::new(b"oasis_audio\0") .priority(16) .spawn(move || { audio_thread_fn(); 0 - }); - if let Err(e) = &audio_result { - psp::dprintln!("OASIS_OS: Failed to spawn audio thread: {:?}", e); + }) + { + // Leak the JoinHandle so the thread isn't killed on drop. + core::mem::forget(handle); } // I/O thread: normal priority (32) for file operations. - let io_result = ThreadBuilder::new(b"oasis_io\0") + if let Ok(handle) = ThreadBuilder::new(b"oasis_io\0") .priority(32) .spawn(move || { io_thread_fn(); 0 - }); - if let Err(e) = &io_result { - psp::dprintln!("OASIS_OS: Failed to spawn I/O thread: {:?}", e); + }) + { + core::mem::forget(handle); } (AudioHandle, IoHandle) @@ -228,14 +199,9 @@ pub fn spawn_workers() -> (AudioHandle, IoHandle) { /// Dedicated audio thread: MP3 playback + SFX mixing. fn audio_thread_fn() { let mut player = AudioPlayer::new(); - if !player.init() { - psp::dprintln!("OASIS_OS: Audio thread init failed"); - } + player.init(); let mut sfx = SfxEngine::new(); - if sfx.is_none() { - psp::dprintln!("OASIS_OS: SFX engine init failed (non-fatal)"); - } loop { match AUDIO_QUEUE.pop() { @@ -243,33 +209,32 @@ fn audio_thread_fn() { if player.load_and_play(&path) { publish_audio_state(&player); } else { - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); } }, Some(AudioCmd::LoadAndPlayData(data)) => { - if player.load_and_play_data(&data) { + if player.load_and_play_owned(data) { publish_audio_state(&player); } else { - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); } }, Some(AudioCmd::Pause) => { if player.is_playing() && !player.is_paused() { player.toggle_pause(); - SHARED_AUDIO.lock().paused = true; + AUDIO_PAUSED.store(true, Ordering::Relaxed); } }, Some(AudioCmd::Resume) => { if player.is_playing() && player.is_paused() { player.toggle_pause(); - SHARED_AUDIO.lock().paused = false; + AUDIO_PAUSED.store(false, Ordering::Relaxed); } }, Some(AudioCmd::Stop) => { player.stop(); - let mut state = SHARED_AUDIO.lock(); - state.playing = false; - state.paused = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); + AUDIO_PAUSED.store(false, Ordering::Relaxed); }, Some(AudioCmd::SetVolume(v)) => { player.set_volume(v); @@ -281,27 +246,21 @@ fn audio_thread_fn() { }, Some(AudioCmd::Shutdown) => { player.stop(); - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); break; }, None => {}, } if player.is_playing() && !player.is_paused() { - // update() contains the blocking sceAudioOutputBlocking call. player.update(); - // Publish position each frame. - { - let mut state = SHARED_AUDIO.lock(); - state.position_ms = player.position_ms(); - state.duration_ms = player.duration_ms(); - } + AUDIO_POSITION_MS.store(player.position_ms() as u32, Ordering::Relaxed); + AUDIO_DURATION_MS.store(player.duration_ms() as u32, Ordering::Relaxed); if !player.is_playing() { - SHARED_AUDIO.lock().playing = false; + AUDIO_PLAYING.store(false, Ordering::Relaxed); } } else { - // Sleep when idle to avoid spinning. - psp::thread::sleep_ms(10); + unsafe { psp::sys::sceKernelDelayThread(10_000) }; } // Pump SFX mixer (separate hardware channel, short blocking). @@ -311,16 +270,16 @@ fn audio_thread_fn() { } } -/// Publish audio player state to the shared spinlock after a load_and_play. +/// Publish audio player state to shared atomics after a load_and_play. fn publish_audio_state(player: &AudioPlayer) { - let mut state = SHARED_AUDIO.lock(); - state.playing = true; - state.paused = false; - state.sample_rate = player.sample_rate; - state.bitrate = player.bitrate; - state.channels = player.channels; - state.position_ms = 0; - state.duration_ms = 0; + AUDIO_SAMPLE_RATE.store(player.sample_rate, Ordering::Relaxed); + AUDIO_BITRATE.store(player.bitrate, Ordering::Relaxed); + AUDIO_CHANNELS.store(player.channels, Ordering::Relaxed); + AUDIO_POSITION_MS.store(0, Ordering::Relaxed); + AUDIO_DURATION_MS.store(0, Ordering::Relaxed); + AUDIO_PAUSED.store(false, Ordering::Relaxed); + // Set playing LAST so readers see consistent metadata first. + AUDIO_PLAYING.store(true, Ordering::Relaxed); } // --------------------------------------------------------------------------- diff --git a/crates/oasis-plugin-psp/src/audio.rs b/crates/oasis-plugin-psp/src/audio.rs index 818bfaa..794a18a 100644 --- a/crates/oasis-plugin-psp/src/audio.rs +++ b/crates/oasis-plugin-psp/src/audio.rs @@ -1282,11 +1282,12 @@ unsafe fn init_audio_drivers() -> bool { // Step 2: Wait for the game to load AVCODEC modules during its own // init, then piggyback on them. This avoids sceUtilityLoadModule - // conflicts. Retry every 2s for up to 60s before falling back to - // loading modules ourselves. + // conflicts. Check every 15s for up to 30s (3 attempts) before + // falling back to loading modules ourselves. Kept infrequent to + // minimise stutter from the NID scan + stub extraction. { let mut attempt = 0u32; - while attempt < 30 { + while attempt < 3 { if unsafe { try_resolve_codec() } { unsafe { core::ptr::write_volatile(&raw mut DECODER_BACKEND, 2); @@ -1304,8 +1305,8 @@ unsafe fn init_audio_drivers() -> bool { return true; } attempt += 1; - if attempt < 30 { - unsafe { psp::sys::sceKernelDelayThread(2_000_000) }; + if attempt < 3 { + unsafe { psp::sys::sceKernelDelayThread(15_000_000) }; } } } @@ -1505,6 +1506,46 @@ unsafe fn set_track_name(path: &[u8]) { } } +// --------------------------------------------------------------------------- +// OASIS OS detection +// --------------------------------------------------------------------------- + +/// Check if the OASIS OS EBOOT is the running application. +/// +/// When running alongside OASIS OS, the PRX must NOT use audio codecs +/// because OASIS OS has its own music player (sceAudiocodec) which shares +/// the ME coprocessor's EDRAM. Both cannot be active simultaneously. +/// +/// Detection: scan the first 1.5MB of user memory for the "OASIS_OS" +/// string embedded in the EBOOT's SceModuleInfo (generated by +/// `psp::module!("OASIS_OS", ...)`). This works from kernel mode +/// without any kernel APIs (which may not be available on all CFW). +unsafe fn is_oasis_running() -> bool { + let needle = b"OASIS_OS\0"; + let start: u32 = 0x0880_0000; + let end: u32 = 0x0898_0000; // First 1.5MB of user memory + let mut addr = start; + while addr <= end - (needle.len() as u32) { + let mut matched = true; + let mut j = 0usize; + while j < needle.len() { + let byte = unsafe { + core::ptr::read_volatile((addr + j as u32) as *const u8) + }; + if byte != needle[j] { + matched = false; + break; + } + j += 1; + } + if matched { + return true; + } + addr += 4; // word-aligned scan + } + false +} + // --------------------------------------------------------------------------- // Audio thread // --------------------------------------------------------------------------- @@ -1513,8 +1554,25 @@ unsafe extern "C" fn audio_thread_entry( _args: usize, _argp: *mut core::ffi::c_void, ) -> i32 { - // Wait for game to initialize before probing for audio modules. - unsafe { psp::sys::sceKernelDelayThread(3_000_000) }; + // Wait for the host application to load before probing. + // OASIS OS EBOOT takes several seconds to boot (progress screens), + // so we check multiple times with increasing delays to catch it. + { + let delays: [u32; 4] = [3_000_000, 3_000_000, 4_000_000, 5_000_000]; + for (i, &delay) in delays.iter().enumerate() { + unsafe { psp::sys::sceKernelDelayThread(delay) }; + if unsafe { is_oasis_running() } { + crate::debug_log( + b"[OASIS] OASIS_OS detected, skipping PRX audio", + ); + return 0; + } + if i < delays.len() - 1 { + crate::debug_log(b"[OASIS] check: OASIS_OS not found, retrying..."); + } + } + crate::debug_log(b"[OASIS] OASIS_OS not detected after 15s, proceeding"); + } if !unsafe { init_audio_drivers() } { crate::debug_log(b"[OASIS] audio init failed");