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();
|
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 pan_raw = expect_int(args, 3)?;
|
||||||
let pitch = expect_number(args, 4, "pitch")?;
|
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
|
if sample_id_raw < 0
|
||||||
|| sample_id_raw > u16::MAX as i64
|
|| sample_id_raw > u16::MAX as i64
|
||||||
|| voice_id_raw < 0
|
|
||||||
|| voice_id_raw >= 16
|
|
||||||
|| !(0..=255).contains(&volume_raw)
|
|| !(0..=255).contains(&volume_raw)
|
||||||
|| !(0..=255).contains(&pan_raw)
|
|| !(0..=255).contains(&pan_raw)
|
||||||
|| !pitch.is_finite()
|
|| !pitch.is_finite()
|
||||||
@ -222,13 +225,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Syscall::AudioPlay => {
|
Syscall::AudioPlay => {
|
||||||
let asset_name = match args
|
let asset_name = expect_string(args, 0, "asset_name")?;
|
||||||
.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 sample_id_raw = expect_int(args, 1)?;
|
let sample_id_raw = expect_int(args, 1)?;
|
||||||
let voice_id_raw = expect_int(args, 2)?;
|
let voice_id_raw = expect_int(args, 2)?;
|
||||||
let volume_raw = expect_int(args, 3)?;
|
let volume_raw = expect_int(args, 3)?;
|
||||||
@ -239,10 +236,13 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
_ => prometeu_hal::LoopMode::On,
|
_ => 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
|
if sample_id_raw < 0
|
||||||
|| sample_id_raw > u16::MAX as i64
|
|| sample_id_raw > u16::MAX as i64
|
||||||
|| voice_id_raw < 0
|
|
||||||
|| voice_id_raw >= 16
|
|
||||||
|| !(0..=255).contains(&volume_raw)
|
|| !(0..=255).contains(&volume_raw)
|
||||||
|| !(0..=255).contains(&pan_raw)
|
|| !(0..=255).contains(&pan_raw)
|
||||||
|| !pitch.is_finite()
|
|| !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)]);
|
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]
|
#[test]
|
||||||
fn tick_asset_commit_operational_error_returns_status_not_crash() {
|
fn tick_asset_commit_operational_error_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
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` returns `status:int`;
|
||||||
- `audio.play_sample` 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`
|
### 11.2 Minimum status table for `play`/`play_sample`
|
||||||
|
|
||||||
- `0` = `OK`
|
- `0` = `OK`
|
||||||
@ -205,4 +212,5 @@ Operational rules:
|
|||||||
|
|
||||||
- no fallback to default bank when an asset cannot be resolved;
|
- no fallback to default bank when an asset cannot be resolved;
|
||||||
- no silent no-op for invalid `voice_id`;
|
- 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.
|
- invalid numeric ranges (e.g. `volume`, `pan`, `pitch`) must return explicit status.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user