PR003: implement audio status-first returns for play operations

This commit is contained in:
bQUARKz 2026-03-09 06:59:28 +00:00
parent fe6931d420
commit 998252aa25
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 117 additions and 36 deletions

View File

@ -1,4 +1,4 @@
use prometeu_hal::AudioBridge; use prometeu_hal::{AudioBridge, AudioOpStatus};
use std::sync::Arc; use std::sync::Arc;
/// Maximum number of simultaneous audio voices supported by the hardware. /// Maximum number of simultaneous audio voices supported by the hardware.
@ -113,7 +113,7 @@ impl AudioBridge for Audio {
pitch: f64, pitch: f64,
priority: u8, priority: u8,
loop_mode: prometeu_hal::LoopMode, loop_mode: prometeu_hal::LoopMode,
) { ) -> AudioOpStatus {
let lm = match loop_mode { let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On, prometeu_hal::LoopMode::On => LoopMode::On,
@ -129,7 +129,7 @@ impl AudioBridge for Audio {
pitch: f64, pitch: f64,
priority: u8, priority: u8,
loop_mode: prometeu_hal::LoopMode, loop_mode: prometeu_hal::LoopMode,
) { ) -> AudioOpStatus {
let lm = match loop_mode { let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On, prometeu_hal::LoopMode::On => LoopMode::On,
@ -177,9 +177,9 @@ impl Audio {
pitch: f64, pitch: f64,
priority: u8, priority: u8,
loop_mode: LoopMode, loop_mode: LoopMode,
) { ) -> AudioOpStatus {
if voice_id >= MAX_CHANNELS { if voice_id >= MAX_CHANNELS {
return; return AudioOpStatus::VoiceInvalid;
} }
// Resolve the sample from the hardware pools // Resolve the sample from the hardware pools
@ -193,14 +193,10 @@ impl Audio {
// "[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.", // "[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.",
// bank_id, sample_id, voice_id // bank_id, sample_id, voice_id
// ); // );
self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode); self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode)
} else {
AudioOpStatus::SampleNotFound
} }
// else {
// eprintln!(
// "[Audio] Failed to resolve sample from bank {} sample {}.",
// bank_id, sample_id
// );
// }
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -213,9 +209,9 @@ impl Audio {
pitch: f64, pitch: f64,
priority: u8, priority: u8,
loop_mode: LoopMode, loop_mode: LoopMode,
) { ) -> AudioOpStatus {
if voice_id >= MAX_CHANNELS { if voice_id >= MAX_CHANNELS {
return; return AudioOpStatus::VoiceInvalid;
} }
// Update local state // Update local state
@ -240,6 +236,7 @@ impl Audio {
priority, priority,
loop_mode, loop_mode,
}); });
AudioOpStatus::Ok
} }
pub fn stop(&mut self, voice_id: usize) { pub fn stop(&mut self, voice_id: usize) {

View File

@ -7,6 +7,17 @@ pub enum LoopMode {
On, On,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum AudioOpStatus {
Ok = 0,
VoiceInvalid = 1,
SampleNotFound = 2,
ArgRangeInvalid = 3,
AssetNotFound = 4,
NoEffect = 5,
}
pub trait AudioBridge { pub trait AudioBridge {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn play( fn play(
@ -19,7 +30,7 @@ pub trait AudioBridge {
pitch: f64, pitch: f64,
priority: u8, priority: u8,
loop_mode: LoopMode, loop_mode: LoopMode,
); ) -> AudioOpStatus;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn play_sample( fn play_sample(
&mut self, &mut self,
@ -30,7 +41,7 @@ pub trait AudioBridge {
pitch: f64, pitch: f64,
priority: u8, priority: u8,
loop_mode: LoopMode, loop_mode: LoopMode,
); ) -> AudioOpStatus;
fn stop(&mut self, voice_id: usize); fn stop(&mut self, voice_id: usize);
fn set_volume(&mut self, voice_id: usize, volume: u8); fn set_volume(&mut self, voice_id: usize, volume: u8);
fn set_pan(&mut self, voice_id: usize, pan: u8); fn set_pan(&mut self, voice_id: usize, pan: u8);

View File

@ -28,7 +28,7 @@ pub mod vm_fault;
pub mod window; pub mod window;
pub use asset_bridge::AssetBridge; pub use asset_bridge::AssetBridge;
pub use audio_bridge::{AudioBridge, LoopMode}; pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
pub use gfx_bridge::{BlendMode, GfxBridge}; pub use gfx_bridge::{BlendMode, GfxBridge};
pub use hardware_bridge::HardwareBridge; pub use hardware_bridge::HardwareBridge;
pub use host_context::{HostContext, HostContextProvider}; pub use host_context::{HostContext, HostContextProvider};

View File

@ -8,7 +8,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
"play_sample", "play_sample",
1, 1,
5, 5,
0, 1,
caps::AUDIO, caps::AUDIO,
Determinism::Deterministic, Determinism::Deterministic,
false, false,
@ -20,7 +20,7 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
"play", "play",
1, 1,
7, 7,
0, 1,
caps::AUDIO, caps::AUDIO,
Determinism::Deterministic, Determinism::Deterministic,
false, false,

View File

@ -7,7 +7,9 @@ use prometeu_hal::sprite::Sprite;
use prometeu_hal::syscalls::Syscall; use prometeu_hal::syscalls::Syscall;
use prometeu_hal::tile::Tile; use prometeu_hal::tile::Tile;
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; use prometeu_hal::{
AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int,
};
impl VirtualMachineRuntime { impl VirtualMachineRuntime {
const GFX_STATUS_OK: i64 = 0; const GFX_STATUS_OK: i64 = 0;
@ -201,22 +203,36 @@ impl NativeInterface for VirtualMachineRuntime {
Ok(()) Ok(())
} }
Syscall::AudioPlaySample => { Syscall::AudioPlaySample => {
let sample_id = expect_int(args, 0)? as u32; let sample_id_raw = expect_int(args, 0)?;
let voice_id = expect_int(args, 1)? as usize; let voice_id_raw = expect_int(args, 1)?;
let volume = expect_int(args, 2)? as u8; let volume_raw = expect_int(args, 2)?;
let pan = expect_int(args, 3)? as u8; let pan_raw = expect_int(args, 3)?;
let pitch = expect_number(args, 4, "pitch")?; let pitch = expect_number(args, 4, "pitch")?;
hw.audio_mut().play( if sample_id_raw < 0
|| sample_id_raw > u16::MAX as i64
|| voice_id_raw < 0
|| voice_id_raw >= 16
|| !(0..=255).contains(&volume_raw)
|| !(0..=255).contains(&pan_raw)
|| !pitch.is_finite()
|| pitch <= 0.0
{
ret.push_int(AudioOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
let status = hw.audio_mut().play(
0, 0,
sample_id as u16, sample_id_raw as u16,
voice_id, voice_id_raw as usize,
volume, volume_raw as u8,
pan, pan_raw as u8,
pitch, pitch,
0, 0,
prometeu_hal::LoopMode::Off, prometeu_hal::LoopMode::Off,
); );
ret.push_int(status as i64);
Ok(()) Ok(())
} }
Syscall::AudioPlay => { Syscall::AudioPlay => {
@ -227,19 +243,44 @@ impl NativeInterface for VirtualMachineRuntime {
Value::String(s) => s.clone(), Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())), _ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
}; };
let sample_id = expect_int(args, 1)? as u16; let sample_id_raw = expect_int(args, 1)?;
let voice_id = expect_int(args, 2)? as usize; let voice_id_raw = expect_int(args, 2)?;
let volume = expect_int(args, 3)? as u8; let volume_raw = expect_int(args, 3)?;
let pan = expect_int(args, 4)? as u8; let pan_raw = expect_int(args, 4)?;
let pitch = expect_number(args, 5, "pitch")?; let pitch = expect_number(args, 5, "pitch")?;
let loop_mode = match expect_int(args, 6)? { let loop_mode = match expect_int(args, 6)? {
0 => prometeu_hal::LoopMode::Off, 0 => prometeu_hal::LoopMode::Off,
_ => prometeu_hal::LoopMode::On, _ => prometeu_hal::LoopMode::On,
}; };
let bank_id = if sample_id_raw < 0
hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0); || sample_id_raw > u16::MAX as i64
hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode); || voice_id_raw < 0
|| voice_id_raw >= 16
|| !(0..=255).contains(&volume_raw)
|| !(0..=255).contains(&pan_raw)
|| !pitch.is_finite()
|| pitch <= 0.0
{
ret.push_int(AudioOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
let Some(bank_id) = hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS) else {
ret.push_int(AudioOpStatus::AssetNotFound as i64);
return Ok(());
};
let status = hw.audio_mut().play(
bank_id,
sample_id_raw as u16,
voice_id_raw as usize,
volume_raw as u8,
pan_raw as u8,
pitch,
0,
loop_mode,
);
ret.push_int(status as i64);
Ok(()) Ok(())
} }
Syscall::FsOpen => { Syscall::FsOpen => {

View File

@ -174,3 +174,35 @@ voices_active: 9
mix_cycles: 410 mix_cycles: 410
audio_commands: 6 audio_commands: 6
``` ```
## 11 Syscall Return and Fault Policy
`audio` follows status-first policy for operations with operational failure modes.
Fault boundary:
- `Trap`: structural ABI misuse (type/arity/capability/shape mismatch);
- `status`: operational failure;
- `Panic`: internal invariant break only.
### 11.1 MVP return shape
In the current MVP:
- `audio.play` returns `status:int`;
- `audio.play_sample` returns `status:int`.
### 11.2 Minimum status table for `play`/`play_sample`
- `0` = `OK`
- `1` = `VOICE_INVALID`
- `2` = `SAMPLE_NOT_FOUND`
- `3` = `ARG_RANGE_INVALID`
- `4` = `ASSET_NOT_FOUND`
- `5` = `NO_EFFECT`
Operational rules:
- no fallback to default bank when an asset cannot be resolved;
- no silent no-op for invalid `voice_id`;
- invalid numeric ranges (e.g. `volume`, `pan`, `pitch`) must return explicit status.