use crate::buffer::BufferFlags;
use crate::clsid::Clsid;
use crate::error::HResult;
use crate::format::{Format, FormatNegotiation};
use crate::realtime::RealtimeContext;
#[derive(Copy, Clone, Debug)]
pub struct ProcessInput<'a> {
samples: &'a [f32],
flags: BufferFlags,
}
impl<'a> ProcessInput<'a> {
#[inline]
#[must_use]
pub const fn new(samples: &'a [f32], flags: BufferFlags) -> Self {
Self { samples, flags }
}
#[inline]
#[must_use]
pub const fn samples(&self) -> &'a [f32] {
self.samples
}
#[inline]
#[must_use]
pub const fn flags(&self) -> BufferFlags {
self.flags
}
#[inline]
#[must_use]
pub const fn is_silent(&self) -> bool {
self.flags.is_silent()
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
pub enum SystemEffectState {
Off,
#[default]
On,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct SystemEffect {
pub id: Clsid,
pub controllable: bool,
pub state: SystemEffectState,
}
impl SystemEffect {
#[inline]
#[must_use]
pub const fn new(id: Clsid) -> Self {
Self {
id,
controllable: false,
state: SystemEffectState::On,
}
}
#[inline]
#[must_use]
pub const fn with_controllable(mut self, controllable: bool) -> Self {
self.controllable = controllable;
self
}
#[inline]
#[must_use]
pub const fn with_state(mut self, state: SystemEffectState) -> Self {
self.state = state;
self
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[non_exhaustive]
pub enum ApoCategory {
Sfx,
Mfx,
Efx,
}
pub trait ProcessingObject: Sized + Send {
const CLSID: Clsid;
const NAME: &'static str;
const COPYRIGHT: &'static str;
const CATEGORY: ApoCategory;
fn new() -> Self;
fn is_input_format_supported(&self, format: &Format) -> FormatNegotiation {
default_float32_negotiation(format)
}
fn is_output_format_supported(&self, format: &Format) -> FormatNegotiation {
default_float32_negotiation(format)
}
fn system_effects(&self) -> &[SystemEffect] {
&[]
}
fn set_system_effect_state(&mut self, id: &Clsid, state: SystemEffectState) {
let _ = (id, state);
}
fn lock_for_process(&mut self, input: &Format, output: &Format) -> Result<(), HResult> {
let _ = (input, output);
Ok(())
}
fn unlock_for_process(&mut self) {}
fn process(
&mut self,
rt: &RealtimeContext,
input: ProcessInput<'_>,
output: &mut [f32],
) -> BufferFlags;
}
#[inline]
fn default_float32_negotiation(format: &Format) -> FormatNegotiation {
if format.is_float() && format.bits_per_sample() == 32 {
FormatNegotiation::Accept
} else {
FormatNegotiation::Suggest(Format::pcm_float32(format.sample_rate(), format.channels()))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Passthrough;
impl ProcessingObject for Passthrough {
const CLSID: Clsid = Clsid::from_u128(0xCAFEBABE_DEAD_BEEF_1234_56789ABCDEF0);
const NAME: &'static str = "tympan-apo passthrough";
const COPYRIGHT: &'static str = "test fixture";
const CATEGORY: ApoCategory = ApoCategory::Sfx;
fn new() -> Self {
Self
}
fn process(
&mut self,
_rt: &RealtimeContext,
input: ProcessInput<'_>,
output: &mut [f32],
) -> BufferFlags {
output.copy_from_slice(input.samples());
input.flags()
}
}
#[test]
fn variants_are_distinct() {
assert_ne!(ApoCategory::Sfx, ApoCategory::Mfx);
assert_ne!(ApoCategory::Mfx, ApoCategory::Efx);
assert_ne!(ApoCategory::Sfx, ApoCategory::Efx);
}
#[test]
fn associated_constants_round_trip() {
assert_eq!(Passthrough::NAME, "tympan-apo passthrough");
assert_eq!(Passthrough::COPYRIGHT, "test fixture");
assert_eq!(Passthrough::CATEGORY, ApoCategory::Sfx);
assert!(!Passthrough::CLSID.is_nil());
}
#[test]
fn default_input_format_accepts_float32_at_any_rate_channels() {
let apo = Passthrough::new();
for (rate, ch) in [(48_000, 1), (44_100, 2), (96_000, 6), (192_000, 8)] {
assert_eq!(
apo.is_input_format_supported(&Format::pcm_float32(rate, ch)),
FormatNegotiation::Accept,
"float32 {rate} Hz × {ch} ch must be accepted",
);
}
}
#[test]
fn default_input_format_suggests_float32_for_int_pcm() {
let apo = Passthrough::new();
for fmt in [
Format::pcm_int16(48_000, 2),
Format::pcm_int24(44_100, 1),
Format::pcm_int32(96_000, 4),
] {
match apo.is_input_format_supported(&fmt) {
FormatNegotiation::Suggest(suggested) => {
assert!(suggested.is_float(), "suggestion must be float");
assert_eq!(suggested.bits_per_sample(), 32);
assert_eq!(suggested.sample_rate(), fmt.sample_rate());
assert_eq!(suggested.channels(), fmt.channels());
}
other => panic!("expected Suggest for {fmt:?}, got {other:?}"),
}
}
}
#[test]
fn default_input_format_suggests_float32_for_float64() {
let apo = Passthrough::new();
let f = Format::pcm_float64(48_000, 1);
match apo.is_input_format_supported(&f) {
FormatNegotiation::Suggest(s) => {
assert!(s.is_float());
assert_eq!(s.bits_per_sample(), 32);
}
other => panic!("expected Suggest, got {other:?}"),
}
}
#[test]
fn default_output_negotiation_matches_input() {
let apo = Passthrough::new();
for fmt in [
Format::pcm_float32(48_000, 1),
Format::pcm_int16(44_100, 2),
Format::pcm_float64(96_000, 6),
] {
assert_eq!(
apo.is_input_format_supported(&fmt),
apo.is_output_format_supported(&fmt),
);
}
}
#[test]
fn default_lock_for_process_succeeds() {
let mut apo = Passthrough::new();
let fmt = Format::pcm_float32(48_000, 1);
assert!(apo.lock_for_process(&fmt, &fmt).is_ok());
}
#[test]
fn default_unlock_is_callable() {
let mut apo = Passthrough::new();
apo.unlock_for_process();
}
#[test]
fn process_runs_against_a_synthetic_buffer() {
let mut apo = Passthrough::new();
let samples = [0.1_f32, -0.2, 0.3, -0.4, 0.5, -0.6, 0.7, -0.8];
let mut output = [0.0_f32; 8];
let rt = unsafe { RealtimeContext::new_unchecked() };
let out_flags = apo.process(
&rt,
ProcessInput::new(&samples, BufferFlags::VALID),
&mut output,
);
assert_eq!(output, samples);
assert_eq!(out_flags, BufferFlags::VALID);
}
#[test]
fn process_input_exposes_samples_and_flags() {
let samples = [1.0_f32, 2.0, 3.0];
let input = ProcessInput::new(&samples, BufferFlags::SILENT);
assert_eq!(input.samples(), &samples);
assert_eq!(input.flags(), BufferFlags::SILENT);
assert!(input.is_silent());
}
#[test]
fn process_input_is_not_silent_when_flag_is_valid() {
let samples = [0.0_f32];
let input = ProcessInput::new(&samples, BufferFlags::VALID);
assert!(!input.is_silent());
}
#[test]
fn process_passes_through_input_flags() {
let mut apo = Passthrough::new();
let rt = unsafe { RealtimeContext::new_unchecked() };
let samples = [0.5_f32; 4];
for f in [
BufferFlags::VALID,
BufferFlags::SILENT,
BufferFlags::INVALID,
] {
let mut output = [0.0_f32; 4];
let out = apo.process(&rt, ProcessInput::new(&samples, f), &mut output);
assert_eq!(out, f);
}
}
}