diff --git a/crates/prometeu-core/src/firmware/firmware_step_splash_screen.rs b/crates/prometeu-core/src/firmware/firmware_step_splash_screen.rs index 8f5e7eaa..ec5c7af6 100644 --- a/crates/prometeu-core/src/firmware/firmware_step_splash_screen.rs +++ b/crates/prometeu-core/src/firmware/firmware_step_splash_screen.rs @@ -1,8 +1,7 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep}; use crate::firmware::prometeu_context::PrometeuContext; -use crate::hardware::LoopMode; -use crate::model::Color; use crate::log::{LogLevel, LogSource}; +use crate::model::Color; #[derive(Debug, Clone)] pub struct SplashScreenStep { @@ -13,9 +12,7 @@ impl SplashScreenStep { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Showing SplashScreen".to_string()); // Play sound on enter - if let Some(sample) = ctx.os.sample_square.clone() { - ctx.hw.audio_mut().play(sample, 0, 255, 127, 1.0, 0, LoopMode::Off); - } + // ctx.hw.audio_mut().play(0, 0, 0, 255, 127, 1.0, 0, LoopMode::Off); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { diff --git a/crates/prometeu-core/src/hardware/asset.rs b/crates/prometeu-core/src/hardware/asset.rs index e83fc6ec..508575d9 100644 --- a/crates/prometeu-core/src/hardware/asset.rs +++ b/crates/prometeu-core/src/hardware/asset.rs @@ -1,5 +1,5 @@ -use crate::hardware::memory_banks::TileBankPoolInstaller; -use crate::model::{AssetEntry, BankStats, BankType, Color, HandleId, LoadStatus, SlotRef, SlotStats, TileBank, TileSize}; +use crate::hardware::memory_banks::{TileBankPoolInstaller, SoundBankPoolInstaller}; +use crate::model::{AssetEntry, BankStats, BankType, Color, HandleId, LoadStatus, SlotRef, SlotStats, TileBank, TileSize, SoundBank, Sample}; use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; use std::thread; @@ -101,12 +101,16 @@ pub struct AssetManager { /// Narrow hardware interfaces gfx_installer: Arc, + sound_installer: Arc, /// Track what is installed in each hardware slot (for stats/info). gfx_slots: Arc; 16]>>, + sound_slots: Arc; 16]>>, /// Residency policy for GFX tile banks. gfx_policy: BankPolicy, + /// Residency policy for sound banks. + sound_policy: BankPolicy, // Commits that are ready to be applied at the next frame boundary. pending_commits: Mutex>, @@ -123,6 +127,7 @@ impl AssetManager { assets: Vec, assets_data: Vec, gfx_installer: Arc, + sound_installer: Arc, ) -> Self { let mut asset_map = HashMap::new(); for entry in assets { @@ -132,8 +137,11 @@ impl AssetManager { Self { assets: Arc::new(RwLock::new(asset_map)), gfx_installer, + sound_installer, gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), + sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))), gfx_policy: BankPolicy::new(), + sound_policy: BankPolicy::new(), handles: Arc::new(RwLock::new(HashMap::new())), next_handle_id: Mutex::new(1), assets_data: Arc::new(RwLock::new(assets_data)), @@ -165,15 +173,28 @@ impl AssetManager { let handle_id = *next_id; *next_id += 1; - // Check if already resident - if let Some(bank) = self.gfx_policy.get_resident(asset_id) { - // Dedup: already resident + // Check if already resident (Dedup) + let already_resident = match entry.bank_type { + BankType::TILES => { + if let Some(bank) = self.gfx_policy.get_resident(asset_id) { + self.gfx_policy.stage(handle_id, bank); + true + } else { false } + } + BankType::SOUNDS => { + if let Some(bank) = self.sound_policy.get_resident(asset_id) { + self.sound_policy.stage(handle_id, bank); + true + } else { false } + } + }; + + if already_resident { self.handles.write().unwrap().insert(handle_id, LoadHandleInfo { _asset_id: asset_id.to_string(), slot, status: LoadStatus::READY, }); - self.gfx_policy.stage(handle_id, bank); return Ok(handle_id); } @@ -184,13 +205,17 @@ impl AssetManager { status: LoadStatus::PENDING, }); - let gfx_policy_resident = Arc::clone(&self.gfx_policy.resident); - let gfx_policy_staging = Arc::clone(&self.gfx_policy.staging); let handles = self.handles.clone(); let assets_data = self.assets_data.clone(); let entry_clone = entry.clone(); let asset_id_clone = asset_id.to_string(); + // Capture policies for the worker thread + let gfx_policy_resident = Arc::clone(&self.gfx_policy.resident); + let gfx_policy_staging = Arc::clone(&self.gfx_policy.staging); + let sound_policy_resident = Arc::clone(&self.sound_policy.resident); + let sound_policy_staging = Arc::clone(&self.sound_policy.staging); + thread::spawn(move || { // Update status to LOADING { @@ -199,7 +224,6 @@ impl AssetManager { if h.status == LoadStatus::PENDING { h.status = LoadStatus::LOADING; } else { - // Might have been canceled return; } } else { @@ -207,45 +231,57 @@ impl AssetManager { } } - // Perform IO and Decode - let result = Self::perform_load(&entry_clone, assets_data); - - match result { - Ok(tilebank) => { - let bank_arc = Arc::new(tilebank); - - // Insert or reuse a resident entry (dedup) - let resident_arc = { - let mut map = gfx_policy_resident.write().unwrap(); - match map.get_mut(&asset_id_clone) { - Some(existing) => { + match entry_clone.bank_type { + BankType::TILES => { + let result = Self::perform_load_tile_bank(&entry_clone, assets_data); + if let Ok(tilebank) = result { + let bank_arc = Arc::new(tilebank); + let resident_arc = { + let mut map = gfx_policy_resident.write().unwrap(); + if let Some(existing) = map.get_mut(&asset_id_clone) { existing.last_used = Instant::now(); existing.loads += 1; Arc::clone(&existing.value) - } - None => { + } else { let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize); map.insert(asset_id_clone, entry); bank_arc } + }; + gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc); + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { + if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; } } - }; - - // Add to staging - gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc); - - // Update status to READY - let mut handles_map = handles.write().unwrap(); - if let Some(h) = handles_map.get_mut(&handle_id) { - if h.status == LoadStatus::LOADING { - h.status = LoadStatus::READY; - } + } else { + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; } } } - Err(_) => { - let mut handles_map = handles.write().unwrap(); - if let Some(h) = handles_map.get_mut(&handle_id) { - h.status = LoadStatus::ERROR; + BankType::SOUNDS => { + let result = Self::perform_load_sound_bank(&entry_clone, assets_data); + if let Ok(soundbank) = result { + let bank_arc = Arc::new(soundbank); + let resident_arc = { + let mut map = sound_policy_resident.write().unwrap(); + if let Some(existing) = map.get_mut(&asset_id_clone) { + existing.last_used = Instant::now(); + existing.loads += 1; + Arc::clone(&existing.value) + } else { + let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize); + map.insert(asset_id_clone, entry); + bank_arc + } + }; + sound_policy_staging.write().unwrap().insert(handle_id, resident_arc); + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { + if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; } + } + } else { + let mut handles_map = handles.write().unwrap(); + if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; } } } } @@ -254,7 +290,7 @@ impl AssetManager { Ok(handle_id) } - fn perform_load(entry: &AssetEntry, assets_data: Arc>>) -> Result { + fn perform_load_tile_bank(entry: &AssetEntry, assets_data: Arc>>) -> Result { if entry.codec != "RAW" { return Err(format!("Unsupported codec: {}", entry.codec)); } @@ -308,6 +344,35 @@ impl AssetManager { }) } + fn perform_load_sound_bank(entry: &AssetEntry, assets_data: Arc>>) -> Result { + if entry.codec != "RAW" { + return Err(format!("Unsupported codec: {}", entry.codec)); + } + + let assets_data = assets_data.read().unwrap(); + + let start = entry.offset as usize; + let end = start + entry.size as usize; + + if end > assets_data.len() { + return Err("Asset offset/size out of bounds".to_string()); + } + + let buffer = &assets_data[start..end]; + + let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32; + + let mut data = Vec::with_capacity(buffer.len() / 2); + for i in (0..buffer.len()).step_by(2) { + if i + 1 < buffer.len() { + data.push(i16::from_le_bytes([buffer[i], buffer[i+1]])); + } + } + + let sample = Arc::new(Sample::new(sample_rate, data)); + Ok(SoundBank::new(vec![sample])) + } + pub fn status(&self, handle: HandleId) -> LoadStatus { self.handles.read().unwrap().get(&handle).map(|h| h.status).unwrap_or(LoadStatus::ERROR) } @@ -332,6 +397,7 @@ impl AssetManager { } } self.gfx_policy.take_staging(handle); + self.sound_policy.take_staging(handle); } pub fn apply_commits(&self) { @@ -341,17 +407,27 @@ impl AssetManager { for handle_id in pending.drain(..) { if let Some(h) = handles.get_mut(&handle_id) { if h.status == LoadStatus::READY { - if let Some(bank) = self.gfx_policy.take_staging(handle_id) { - if h.slot.asset_type == BankType::TILES { - self.gfx_installer.install_tile_bank(h.slot.index, bank); - - // Update internal tracking of what's in the slot - let mut slots = self.gfx_slots.write().unwrap(); - if h.slot.index < slots.len() { - slots[h.slot.index] = Some(h._asset_id.clone()); + match h.slot.asset_type { + BankType::TILES => { + if let Some(bank) = self.gfx_policy.take_staging(handle_id) { + self.gfx_installer.install_tile_bank(h.slot.index, bank); + let mut slots = self.gfx_slots.write().unwrap(); + if h.slot.index < slots.len() { + slots[h.slot.index] = Some(h._asset_id.clone()); + } + h.status = LoadStatus::COMMITTED; + } + } + BankType::SOUNDS => { + if let Some(bank) = self.sound_policy.take_staging(handle_id) { + self.sound_installer.install_sound_bank(h.slot.index, bank); + let mut slots = self.sound_slots.write().unwrap(); + if h.slot.index < slots.len() { + slots[h.slot.index] = Some(h._asset_id.clone()); + } + h.status = LoadStatus::COMMITTED; } } - h.status = LoadStatus::COMMITTED; } } } @@ -392,6 +468,38 @@ impl AssetManager { slot_count: 16, } } + BankType::SOUNDS => { + let mut used_bytes = 0; + { + let resident = self.sound_policy.resident.read().unwrap(); + for entry in resident.values() { + used_bytes += entry.bytes; + } + } + + let mut inflight_bytes = 0; + { + let staging = self.sound_policy.staging.read().unwrap(); + let assets = self.assets.read().unwrap(); + let handles = self.handles.read().unwrap(); + + for (handle_id, _) in staging.iter() { + if let Some(h) = handles.get(handle_id) { + if let Some(entry) = assets.get(&h._asset_id) { + inflight_bytes += entry.decoded_size as usize; + } + } + } + } + + BankStats { + total_bytes: 32 * 1024 * 1024, + used_bytes, + free_bytes: (32usize * 1024 * 1024).saturating_sub(used_bytes), + inflight_bytes, + slot_count: 16, + } + } } } @@ -410,6 +518,25 @@ impl AssetManager { 0 }; + SlotStats { + asset_id, + generation: 0, + resident_bytes: bytes, + } + } + BankType::SOUNDS => { + let slots = self.sound_slots.read().unwrap(); + let asset_id = slots.get(slot.index).and_then(|s| s.clone()); + + let bytes = if let Some(id) = &asset_id { + self.sound_policy.resident.read().unwrap() + .get(id) + .map(|entry| entry.bytes) + .unwrap_or(0) + } else { + 0 + }; + SlotStats { asset_id, generation: 0, @@ -421,21 +548,24 @@ impl AssetManager { pub fn shutdown(&self) { self.gfx_policy.clear(); + self.sound_policy.clear(); self.handles.write().unwrap().clear(); self.pending_commits.lock().unwrap().clear(); self.gfx_slots.write().unwrap().fill(None); + self.sound_slots.write().unwrap().fill(None); } } #[cfg(test)] mod tests { use super::*; - use crate::hardware::memory_banks::{TileBankPoolAccess, MemoryBanks}; + use crate::hardware::memory_banks::{TileBankPoolAccess, SoundBankPoolAccess, MemoryBanks}; #[test] fn test_asset_loading_flow() { let banks = Arc::new(MemoryBanks::new()); let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; let mut data = vec![1u8; 256]; data.extend_from_slice(&[0u8; 2048]); @@ -454,7 +584,7 @@ mod tests { }), }; - let am = AssetManager::new(vec![asset_entry], data, gfx_installer); + let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); let slot = SlotRef::gfx(0); let handle = am.load("test_tiles", slot).expect("Should start loading"); @@ -484,6 +614,7 @@ mod tests { fn test_asset_dedup() { let banks = Arc::new(MemoryBanks::new()); let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; let mut data = vec![1u8; 256]; data.extend_from_slice(&[0u8; 2048]); @@ -502,7 +633,7 @@ mod tests { }), }; - let am = AssetManager::new(vec![asset_entry], data, gfx_installer); + let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap(); let start = Instant::now(); @@ -518,4 +649,44 @@ mod tests { let bank2 = staging.get(&handle2).unwrap(); assert!(Arc::ptr_eq(bank1, bank2)); } + + #[test] + fn test_sound_asset_loading() { + let banks = Arc::new(MemoryBanks::new()); + let gfx_installer = Arc::clone(&banks) as Arc; + let sound_installer = Arc::clone(&banks) as Arc; + + // 100 samples of 16-bit PCM (zeros) + let data = vec![0u8; 200]; + + let asset_entry = AssetEntry { + asset_id: "test_sound".to_string(), + bank_type: BankType::SOUNDS, + offset: 0, + size: data.len() as u64, + decoded_size: data.len() as u64, + codec: "RAW".to_string(), + metadata: serde_json::json!({ + "sample_rate": 44100 + }), + }; + + let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); + let slot = SlotRef::audio(0); + + let handle = am.load("test_sound", slot).expect("Should start loading"); + + let start = Instant::now(); + while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 { + thread::sleep(std::time::Duration::from_millis(10)); + } + + assert_eq!(am.status(handle), LoadStatus::READY); + + am.commit(handle); + am.apply_commits(); + + assert_eq!(am.status(handle), LoadStatus::COMMITTED); + assert!(banks.sound_bank_slot(0).is_some()); + } } diff --git a/crates/prometeu-core/src/hardware/audio.rs b/crates/prometeu-core/src/hardware/audio.rs index 849cc3dd..9bc32ca6 100644 --- a/crates/prometeu-core/src/hardware/audio.rs +++ b/crates/prometeu-core/src/hardware/audio.rs @@ -1,3 +1,4 @@ +use crate::hardware::memory_banks::SoundBankPoolAccess; use crate::model::Sample; use std::sync::Arc; @@ -21,8 +22,10 @@ pub enum LoopMode { /// The Core maintains this state to provide information to the App (e.g., is_playing), /// but the actual real-time mixing is performed by the Host using commands. pub struct Channel { - /// Reference to the PCM data being played. + /// The actual sample data being played. pub sample: Option>, + /// Whether this channel is currently active. + pub active: bool, /// Current playback position within the sample (fractional for pitch shifting). pub pos: f64, /// Playback speed multiplier (1.0 = original speed). @@ -41,6 +44,7 @@ impl Default for Channel { fn default() -> Self { Self { sample: None, + active: false, pos: 0.0, pitch: 1.0, volume: 255, @@ -97,68 +101,83 @@ pub struct Audio { pub voices: [Channel; MAX_CHANNELS], /// Queue of pending commands to be processed by the Host mixer. pub commands: Vec, + /// Interface to access sound memory banks. + pub sound_banks: Arc, } impl Audio { - /// Initializes the audio system with empty voices. - pub fn new() -> Self { - const EMPTY_CHANNEL: Channel = Channel { - sample: None, - pos: 0.0, - pitch: 1.0, - volume: 255, - pan: 127, - loop_mode: LoopMode::Off, - priority: 0, - }; - + /// Initializes the audio system with empty voices and sound bank access. + pub fn new(sound_banks: Arc) -> Self { Self { - voices: [EMPTY_CHANNEL; MAX_CHANNELS], + voices: std::array::from_fn(|_| Channel::default()), commands: Vec::new(), + sound_banks, } } - pub fn play(&mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { - if voice_id < MAX_CHANNELS { - self.commands.push(AudioCommand::Play { - sample, - voice_id, - volume, - pan, - pitch, - priority, - loop_mode, - }); + pub fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { + if voice_id >= MAX_CHANNELS { + return; } + + // Resolve the sample from the hardware pools + let sample = self.sound_banks.sound_bank_slot(bank_id as usize) + .and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone)); + + + if let Some(s) = sample { + self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode); + } + } + + pub fn play_sample(&mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { + if voice_id >= MAX_CHANNELS { + return; + } + // Push command to the host + self.commands.push(AudioCommand::Play { + sample, + voice_id, + volume, + pan, + pitch, + priority, + loop_mode, + }); } pub fn stop(&mut self, voice_id: usize) { if voice_id < MAX_CHANNELS { + self.voices[voice_id].active = false; + self.voices[voice_id].sample = None; self.commands.push(AudioCommand::Stop { voice_id }); } } pub fn set_volume(&mut self, voice_id: usize, volume: u8) { if voice_id < MAX_CHANNELS { + self.voices[voice_id].volume = volume; self.commands.push(AudioCommand::SetVolume { voice_id, volume }); } } pub fn set_pan(&mut self, voice_id: usize, pan: u8) { if voice_id < MAX_CHANNELS { + self.voices[voice_id].pan = pan; self.commands.push(AudioCommand::SetPan { voice_id, pan }); } } pub fn set_pitch(&mut self, voice_id: usize, pitch: f64) { if voice_id < MAX_CHANNELS { + self.voices[voice_id].pitch = pitch; self.commands.push(AudioCommand::SetPitch { voice_id, pitch }); } } pub fn is_playing(&self, voice_id: usize) -> bool { if voice_id < MAX_CHANNELS { - self.voices[voice_id].sample.is_some() + self.voices[voice_id].active } else { false } diff --git a/crates/prometeu-core/src/hardware/hardware.rs b/crates/prometeu-core/src/hardware/hardware.rs index d8075493..5e7925fd 100644 --- a/crates/prometeu-core/src/hardware/hardware.rs +++ b/crates/prometeu-core/src/hardware/hardware.rs @@ -1,5 +1,5 @@ use crate::hardware::{AssetManager, Audio, Gfx, HardwareBridge, Pad, Touch, MemoryBanks}; -use crate::hardware::memory_banks::{TileBankPoolAccess, TileBankPoolInstaller}; +use crate::hardware::memory_banks::{TileBankPoolAccess, TileBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller}; use std::sync::Arc; /// Aggregate structure for all virtual hardware peripherals. @@ -36,8 +36,6 @@ impl HardwareBridge for Hardware { fn assets(&self) -> &AssetManager { &self.assets } fn assets_mut(&mut self) -> &mut AssetManager { &mut self.assets } - - fn memory_banks(&self) -> &MemoryBanks { &self.memory_banks } } impl Hardware { @@ -52,13 +50,14 @@ impl Hardware { Self { memory_banks: Arc::clone(&memory_banks), gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc), - audio: Audio::new(), + audio: Audio::new(Arc::clone(&memory_banks) as Arc), pad: Pad::default(), touch: Touch::default(), assets: AssetManager::new( vec![], vec![], Arc::clone(&memory_banks) as Arc, + Arc::clone(&memory_banks) as Arc, ), } } diff --git a/crates/prometeu-core/src/hardware/memory_banks.rs b/crates/prometeu-core/src/hardware/memory_banks.rs index 8dd26519..2c6d86d9 100644 --- a/crates/prometeu-core/src/hardware/memory_banks.rs +++ b/crates/prometeu-core/src/hardware/memory_banks.rs @@ -1,5 +1,5 @@ use std::sync::{Arc, RwLock}; -use crate::model::TileBank; +use crate::model::{TileBank, SoundBank}; /// Non-generic interface for peripherals to access graphical tile banks. pub trait TileBankPoolAccess: Send + Sync { @@ -15,6 +15,20 @@ pub trait TileBankPoolInstaller: Send + Sync { fn install_tile_bank(&self, slot: usize, bank: Arc); } +/// Non-generic interface for peripherals to access sound banks. +pub trait SoundBankPoolAccess: Send + Sync { + /// Returns a reference to the resident SoundBank in the specified slot, if any. + fn sound_bank_slot(&self, slot: usize) -> Option>; + /// Returns the total number of slots available in this bank. + fn sound_bank_slot_count(&self) -> usize; +} + +/// Non-generic interface for the AssetManager to install sound banks. +pub trait SoundBankPoolInstaller: Send + Sync { + /// Atomically swaps the resident SoundBank in the specified slot. + fn install_sound_bank(&self, slot: usize, bank: Arc); +} + /// Centralized container for all hardware memory banks. /// /// MemoryBanks represent the actual hardware slot state. @@ -22,6 +36,7 @@ pub trait TileBankPoolInstaller: Send + Sync { /// AssetManager coordinates residency and installs assets into these slots. pub struct MemoryBanks { tile_bank_pool: Arc>; 16]>>, + sound_bank_pool: Arc>; 16]>>, } impl MemoryBanks { @@ -29,6 +44,7 @@ impl MemoryBanks { pub fn new() -> Self { Self { tile_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))), + sound_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))), } } } @@ -52,3 +68,23 @@ impl TileBankPoolInstaller for MemoryBanks { } } } + +impl SoundBankPoolAccess for MemoryBanks { + fn sound_bank_slot(&self, slot: usize) -> Option> { + let pool = self.sound_bank_pool.read().unwrap(); + pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone)) + } + + fn sound_bank_slot_count(&self) -> usize { + 16 + } +} + +impl SoundBankPoolInstaller for MemoryBanks { + fn install_sound_bank(&self, slot: usize, bank: Arc) { + let mut pool = self.sound_bank_pool.write().unwrap(); + if slot < 16 { + pool[slot] = Some(bank); + } + } +} diff --git a/crates/prometeu-core/src/hardware/mod.rs b/crates/prometeu-core/src/hardware/mod.rs index 6e3e8f7c..8e240f35 100644 --- a/crates/prometeu-core/src/hardware/mod.rs +++ b/crates/prometeu-core/src/hardware/mod.rs @@ -7,15 +7,15 @@ mod audio; mod memory_banks; pub mod hardware; -pub use asset::AssetManager; pub use crate::model::HandleId; -pub use gfx::Gfx; +pub use asset::AssetManager; +pub use audio::{Audio, AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; pub use gfx::BlendMode; +pub use gfx::Gfx; pub use input_signal::InputSignals; +pub use memory_banks::MemoryBanks; pub use pad::Pad; pub use touch::Touch; -pub use audio::{Audio, AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; -pub use memory_banks::MemoryBanks; pub trait HardwareBridge { fn gfx(&self) -> &Gfx; @@ -32,6 +32,4 @@ pub trait HardwareBridge { fn assets(&self) -> &AssetManager; fn assets_mut(&mut self) -> &mut AssetManager; - - fn memory_banks(&self) -> &MemoryBanks; } diff --git a/crates/prometeu-core/src/model/asset.rs b/crates/prometeu-core/src/model/asset.rs index bbd5d7af..83bd0745 100644 --- a/crates/prometeu-core/src/model/asset.rs +++ b/crates/prometeu-core/src/model/asset.rs @@ -5,8 +5,8 @@ pub type HandleId = u32; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[allow(non_camel_case_types)] pub enum BankType { - TILES, // TILE_BANK - // SOUNDS, + TILES, + SOUNDS, // TILEMAPS, // BLOBS, } @@ -61,4 +61,11 @@ impl SlotRef { index, } } + + pub fn audio(index: usize) -> Self { + Self { + asset_type: BankType::SOUNDS, + index, + } + } } diff --git a/crates/prometeu-core/src/model/mod.rs b/crates/prometeu-core/src/model/mod.rs index 5af6e6e1..f90529cf 100644 --- a/crates/prometeu-core/src/model/mod.rs +++ b/crates/prometeu-core/src/model/mod.rs @@ -4,6 +4,7 @@ mod button; mod tile; mod tile_layer; mod tile_bank; +mod sound_bank; mod sprite; mod sample; mod cartridge; @@ -16,6 +17,7 @@ pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError}; pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader}; pub use color::Color; pub use sample::Sample; +pub use sound_bank::SoundBank; pub use sprite::Sprite; pub use tile::Tile; pub use tile_bank::{TileBank, TileSize}; diff --git a/crates/prometeu-core/src/model/sound_bank.rs b/crates/prometeu-core/src/model/sound_bank.rs new file mode 100644 index 00000000..fbf70218 --- /dev/null +++ b/crates/prometeu-core/src/model/sound_bank.rs @@ -0,0 +1,16 @@ +use crate::model::Sample; +use std::sync::Arc; + +/// A container for audio assets. +/// +/// A SoundBank stores multiple audio samples that can be played by the +/// audio subsystem. +pub struct SoundBank { + pub samples: Vec>, +} + +impl SoundBank { + pub fn new(samples: Vec>) -> Self { + Self { samples } + } +} diff --git a/crates/prometeu-core/src/model/tile_layer.rs b/crates/prometeu-core/src/model/tile_layer.rs index e0e0e8b7..f2153491 100644 --- a/crates/prometeu-core/src/model/tile_layer.rs +++ b/crates/prometeu-core/src/model/tile_layer.rs @@ -35,7 +35,7 @@ impl TileLayer { fn create(width: usize, height: usize, tile_size: TileSize) -> Self { Self { bank_id: 0, - tile_size: tile_size, + tile_size, map: TileMap::create(width, height), } } diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 2feedac5..924428de 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -1,12 +1,11 @@ use crate::fs::{FsBackend, FsState, VirtualFS}; use crate::hardware::{HardwareBridge, InputSignals}; use crate::log::{LogLevel, LogService, LogSource}; -use crate::model::{Cartridge, Color, Sample}; +use crate::model::{Cartridge, Color}; use crate::prometeu_os::{NativeInterface, Syscall}; use crate::telemetry::{CertificationConfig, Certifier, TelemetryFrame}; use crate::virtual_machine::{Value, VirtualMachine}; use std::collections::HashMap; -use std::sync::Arc; use std::time::Instant; /// PrometeuOS (POS): The system firmware/base. @@ -25,11 +24,6 @@ pub struct PrometeuOS { /// Real-world CPU time (in microseconds) consumed by the last host tick. pub last_frame_cpu_time_us: u64, - // Example assets (kept for compatibility with v0 audio syscalls) - pub sample_square: Option>, - pub sample_kick: Option>, - pub sample_snare: Option>, - // Filesystem /// The virtual filesystem interface. pub fs: VirtualFS, @@ -87,9 +81,6 @@ impl PrometeuOS { logical_frame_active: false, logical_frame_remaining_cycles: 0, last_frame_cpu_time_us: 0, - sample_square: None, - sample_kick: None, - sample_snare: None, fs: VirtualFS::new(), fs_state: FsState::Unmounted, open_files: HashMap::new(), @@ -108,9 +99,6 @@ impl PrometeuOS { boot_time, }; - // Initializes basic samples (same logic as LogicalHardware) - os.sample_square = Some(Arc::new(Self::create_square_sample(440.0, 0.1))); - os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string()); os @@ -367,24 +355,6 @@ impl PrometeuOS { _ => false, } } - - fn create_square_sample(freq: f64, duration: f64) -> Sample { - let sample_rate = crate::hardware::OUTPUT_SAMPLE_RATE; - let num_samples = (duration * sample_rate as f64) as usize; - let mut data = Vec::with_capacity(num_samples); - let period = sample_rate as f64 / freq; - - for i in 0..num_samples { - let val = if (i as f64 % period) < (period / 2.0) { - 10000 - } else { - -10000 - }; - data.push(val); - } - - Sample::new(sample_rate, data) - } } #[cfg(test)] @@ -725,16 +695,17 @@ impl NativeInterface for PrometeuOS { let voice_id = vm.pop_integer()? as usize; let sample_id = vm.pop_integer()? as u32; - let sample = match sample_id { - 0 => self.sample_square.clone(), - 1 => self.sample_kick.clone(), - 2 => self.sample_snare.clone(), - _ => None, - }; + // let sample = match sample_id { + // 0 => self.sample_square.clone(), + // 1 => self.sample_kick.clone(), + // 2 => self.sample_snare.clone(), + // _ => None, + // }; - if let Some(s) = sample { - hw.audio_mut().play(s, voice_id, volume, pan, pitch, 0, crate::hardware::LoopMode::Off); - } + // if let Some(s) = sample { + // hw.audio_mut().play(s, voice_id, volume, pan, pitch, 0, crate::hardware::LoopMode::Off); + // } + hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, crate::hardware::LoopMode::Off); vm.push(Value::Null); Ok(300) } diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index d823736c..cbe2c85e 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -688,9 +688,9 @@ impl VirtualMachine { #[cfg(test)] mod tests { use super::*; - use crate::virtual_machine::Value; - use crate::prometeu_os::NativeInterface; use crate::hardware::HardwareBridge; + use crate::prometeu_os::NativeInterface; + use crate::virtual_machine::Value; struct MockNative; impl NativeInterface for MockNative { @@ -711,7 +711,6 @@ mod tests { fn touch_mut(&mut self) -> &mut crate::hardware::Touch { todo!() } fn assets(&self) -> &crate::hardware::AssetManager { todo!() } fn assets_mut(&mut self) -> &mut crate::hardware::AssetManager { todo!() } - fn memory_banks(&self) -> &crate::hardware::MemoryBanks { todo!() } } #[test] diff --git a/crates/prometeu-runtime-desktop/src/audio.rs b/crates/prometeu-runtime-desktop/src/audio.rs index ae194f5b..89e3f77a 100644 --- a/crates/prometeu-runtime-desktop/src/audio.rs +++ b/crates/prometeu-runtime-desktop/src/audio.rs @@ -1,5 +1,5 @@ -use prometeu_core::hardware::{AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use prometeu_core::hardware::{AudioCommand, Channel, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; use ringbuf::traits::{Consumer, Producer, Split}; use ringbuf::HeapRb; use std::sync::Arc; @@ -110,6 +110,7 @@ impl AudioMixer { if voice_id < MAX_CHANNELS { self.voices[voice_id] = Channel { sample: Some(sample), + active: true, pos: 0.0, pitch, volume, @@ -121,6 +122,7 @@ impl AudioMixer { } AudioCommand::Stop { voice_id } => { if voice_id < MAX_CHANNELS { + self.voices[voice_id].active = false; self.voices[voice_id].sample = None; } } @@ -168,6 +170,7 @@ impl AudioMixer { let pos_fract = voice.pos - pos_int as f64; if pos_int >= sample_data.data.len() { + voice.active = false; voice.sample = None; break; } @@ -197,6 +200,7 @@ impl AudioMixer { let loop_start = sample_data.loop_start.unwrap_or(0) as f64; voice.pos = loop_start + (voice.pos - end_pos); } else { + voice.active = false; voice.sample = None; break; }