Audio Status-First Surface and Fault Matrix
This commit is contained in:
parent
fd5a5cd22c
commit
9cb0e77b01
@ -277,3 +277,54 @@ impl Audio {
|
||||
self.commands.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller};
|
||||
use prometeu_hal::sound_bank::SoundBank;
|
||||
|
||||
fn sample() -> Arc<Sample> {
|
||||
Arc::new(Sample::new(44_100, vec![0, 1, 0, -1]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_returns_voice_invalid_for_out_of_range_voice() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let sound_banks = Arc::clone(&banks) as Arc<dyn SoundBankPoolAccess>;
|
||||
let mut audio = Audio::new(sound_banks);
|
||||
|
||||
let status = audio.play(0, 0, MAX_CHANNELS, 255, 128, 1.0, 0, LoopMode::Off);
|
||||
|
||||
assert_eq!(status, AudioOpStatus::VoiceInvalid);
|
||||
assert!(audio.commands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_returns_sample_not_found_when_sample_is_missing() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||
installer.install_sound_bank(0, Arc::new(SoundBank::new(vec![sample()])));
|
||||
let sound_banks = Arc::clone(&banks) as Arc<dyn SoundBankPoolAccess>;
|
||||
let mut audio = Audio::new(sound_banks);
|
||||
|
||||
let status = audio.play(0, 1, 0, 255, 128, 1.0, 0, LoopMode::Off);
|
||||
|
||||
assert_eq!(status, AudioOpStatus::SampleNotFound);
|
||||
assert!(audio.commands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_sample_returns_voice_invalid_without_side_effects() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let sound_banks = Arc::clone(&banks) as Arc<dyn SoundBankPoolAccess>;
|
||||
let mut audio = Audio::new(sound_banks);
|
||||
|
||||
let status =
|
||||
audio.play_sample(sample(), MAX_CHANNELS, 255, 128, 1.0, 0, LoopMode::Off);
|
||||
|
||||
assert_eq!(status, AudioOpStatus::VoiceInvalid);
|
||||
assert!(!audio.voices.iter().any(|voice| voice.active));
|
||||
assert!(audio.commands.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,10 +195,13 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
let pan_raw = expect_int(args, 3)?;
|
||||
let pitch = expect_number(args, 4, "pitch")?;
|
||||
|
||||
if voice_id_raw < 0 || voice_id_raw >= 16 {
|
||||
ret.push_int(AudioOpStatus::VoiceInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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()
|
||||
@ -222,13 +225,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
Ok(())
|
||||
}
|
||||
Syscall::AudioPlay => {
|
||||
let asset_name = match args
|
||||
.first()
|
||||
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
|
||||
};
|
||||
let asset_name = expect_string(args, 0, "asset_name")?;
|
||||
let sample_id_raw = expect_int(args, 1)?;
|
||||
let voice_id_raw = expect_int(args, 2)?;
|
||||
let volume_raw = expect_int(args, 3)?;
|
||||
@ -239,10 +236,13 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
_ => prometeu_hal::LoopMode::On,
|
||||
};
|
||||
|
||||
if voice_id_raw < 0 || voice_id_raw >= 16 {
|
||||
ret.push_int(AudioOpStatus::VoiceInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@ -447,6 +447,102 @@ fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_audio_play_voice_invalid_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 16\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("missing_sound_bank".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "audio".into(),
|
||||
name: "play".into(),
|
||||
version: 1,
|
||||
arg_slots: 7,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "invalid voice must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_audio_play_missing_asset_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("missing_sound_bank".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "audio".into(),
|
||||
name: "play".into(),
|
||||
version: 1,
|
||||
arg_slots: 7,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "missing audio asset must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::AssetNotFound as i64)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![SyscallDecl {
|
||||
module: "audio".into(),
|
||||
name: "play".into(),
|
||||
version: 1,
|
||||
arg_slots: 7,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime
|
||||
.tick(&mut vm, &signals, &mut hardware)
|
||||
.expect("type mismatch must surface as trap");
|
||||
match report {
|
||||
CrashReport::VmTrap { trap } => {
|
||||
assert_eq!(trap.code, TRAP_TYPE);
|
||||
assert!(trap.message.contains("Expected string asset_name"));
|
||||
}
|
||||
other => panic!("expected VmTrap crash report, got {:?}", other),
|
||||
}
|
||||
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_asset_commit_operational_error_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
# PR003 - Audio Status-First Surface and Fault Matrix
|
||||
|
||||
## Briefing
|
||||
|
||||
A decision `009` fixou que `audio` deve seguir status-first.
|
||||
|
||||
No MVP atual, `audio.play` e `audio.play_sample` devem retornar `status:int`.
|
||||
|
||||
## Alvo
|
||||
|
||||
Aplicar a decisao `009` em spec e runtime para o dominio `audio`.
|
||||
|
||||
Arquivos principais:
|
||||
|
||||
- `docs/runtime/specs/05-audio-peripheral.md`
|
||||
- `crates/console/prometeu-hal/src/syscalls/domains/audio.rs`
|
||||
- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs`
|
||||
- `crates/console/prometeu-drivers/src/audio.rs`
|
||||
|
||||
## Escopo Funcional
|
||||
|
||||
- mudar surface de retorno de `audio.play` e `audio.play_sample` para `status`;
|
||||
- remover fallback implicito e no-op silencioso para erros operacionais;
|
||||
- validar faixas normativas (`volume`, `pan`, `pitch`) com status explicito;
|
||||
- limpar caminhos que poderiam escalar para `Panic` por input de app.
|
||||
|
||||
## Fora de Escopo
|
||||
|
||||
- novos comandos de audio (stop/set pan/set pitch etc) fora do contrato atual;
|
||||
- mudanca de arquitetura do mixer host.
|
||||
|
||||
## Critérios de Aceite
|
||||
|
||||
- `ret_slots` de `audio.play` e `audio.play_sample` atualizados no registry;
|
||||
- casos operacionais retornam status inteiro canonico;
|
||||
- `voice_id` invalido e asset/sample ausente nao ficam silenciosos;
|
||||
- sem `Panic` operacional no dominio.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-drivers`
|
||||
- cenarios com `voice_id` invalido, asset ausente e range invalido.
|
||||
@ -192,6 +192,13 @@ In the current MVP:
|
||||
- `audio.play` returns `status:int`;
|
||||
- `audio.play_sample` returns `status:int`.
|
||||
|
||||
Return-shape matrix in v1 syscall surface:
|
||||
|
||||
| Syscall | Return | Policy basis |
|
||||
| --------------------- | ------------ | -------------------------------------- |
|
||||
| `audio.play` | `status:int` | operational rejection is observable |
|
||||
| `audio.play_sample` | `status:int` | operational rejection is observable |
|
||||
|
||||
### 11.2 Minimum status table for `play`/`play_sample`
|
||||
|
||||
- `0` = `OK`
|
||||
@ -205,4 +212,5 @@ Operational rules:
|
||||
|
||||
- no fallback to default bank when an asset cannot be resolved;
|
||||
- no silent no-op for invalid `voice_id`;
|
||||
- invalid `voice_id` must return `VOICE_INVALID`, not `ARG_RANGE_INVALID`;
|
||||
- invalid numeric ranges (e.g. `volume`, `pan`, `pitch`) must return explicit status.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user