use crate::{
Audio, DType, Error, Matrix, OpAttribute, Result,
runtime::{
AudioSemanticDispatch, AudioSemanticOutput, BufferBinding, ComputeDispatch, KernelId,
PushConstant,
},
};
use super::AudioChannelLayout;
const BIQUAD_BLOCK_SIZE: u32 = 256;
fn allocate_like(input: &Audio) -> Result<Matrix> {
Matrix::allocate(
input.engine_handle(),
input.as_matrix().shape().to_vec(),
input.as_matrix().num_elements(),
crate::DType::F32,
)
}
fn wrap_like(matrix: Matrix, input: &Audio) -> Result<Audio> {
Audio::new(matrix, input.sample_rate(), input.layout())
}
pub(super) fn element_count_u32(input: &Audio, operation: &str) -> Result<u32> {
u32::try_from(input.as_matrix().num_elements())
.map_err(|_| Error::out_of_range(format!("{operation} dispatch exceeds u32")))
}
pub(super) fn u32_extent(value: usize, label: &str) -> Result<u32> {
u32::try_from(value).map_err(|_| Error::out_of_range(format!("{label} exceeds u32")))
}
pub(super) fn float_attribute(name: &str, value: f32) -> OpAttribute {
OpAttribute::Float {
name: name.into(),
value: f64::from(value),
}
}
pub(super) fn unsigned_attribute(name: &str, value: u64) -> OpAttribute {
OpAttribute::UnsignedInteger {
name: name.into(),
value,
}
}
fn gcd(mut left: u32, mut right: u32) -> u32 {
while right != 0 {
let remainder = left % right;
left = right;
right = remainder;
}
left
}
fn stable_biquad(coefficients: BiquadCoefficients) -> bool {
if ![
coefficients.b0,
coefficients.b1,
coefficients.b2,
coefficients.a1,
coefficients.a2,
]
.into_iter()
.all(f32::is_finite)
{
return false;
}
let a1 = f64::from(coefficients.a1);
let a2 = f64::from(coefficients.a2);
1.0 + a1 + a2 > 0.0 && 1.0 - a1 + a2 > 0.0 && 1.0 - a2 > 0.0
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NormalizeAudioMode {
#[default]
Peak,
Rms,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NormalizeAudioConfig {
pub target_db: f32,
pub mode: NormalizeAudioMode,
}
impl Default for NormalizeAudioConfig {
fn default() -> Self {
Self {
target_db: -3.0,
mode: NormalizeAudioMode::Peak,
}
}
}
pub fn normalize(input: &Audio, config: NormalizeAudioConfig) -> Result<Audio> {
if !config.target_db.is_finite() || !(-300.0..=100.0).contains(&config.target_db) {
return Err(Error::invalid_argument(
"audio::normalize target_db must be finite and in [-300, 100]",
));
}
let count = element_count_u32(input, "audio::normalize")?;
let level = Matrix::allocate(input.engine_handle(), vec![1], 1, crate::DType::F32)?;
let output_matrix = allocate_like(input)?;
let output = wrap_like(output_matrix, input)?;
let level_buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(level.storage()),
];
let mode = match config.mode {
NormalizeAudioMode::Peak => 0,
NormalizeAudioMode::Rms => 1,
};
let level_push = [PushConstant::U32(count), PushConstant::U32(mode)];
let apply_buffers = [
BufferBinding::read(input.storage()),
BufferBinding::read(level.storage()),
BufferBinding::write(output.storage()),
];
let apply_push = [
PushConstant::U32(count),
PushConstant::F32(10.0_f32.powf(config.target_db / 20.0)),
];
let dispatches = [
ComputeDispatch {
kernel: KernelId::AudioNormalizeLevelF32,
buffers: &level_buffers,
push_constants: &level_push,
workgroups: [1, 1, 1],
},
ComputeDispatch {
kernel: KernelId::AudioNormalizeApplyF32,
buffers: &apply_buffers,
push_constants: &apply_push,
workgroups: KernelId::AudioNormalizeApplyF32.linear_workgroups(count),
},
];
let attributes = [
float_attribute("target_db", config.target_db),
unsigned_attribute("mode", u64::from(mode)),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_split_semantic(
&dispatches,
AudioSemanticDispatch {
contract: crate::core::operation::audio::NORMALIZE,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ResampleConfig {
pub out_rate: u32,
pub filter_half_width: u32,
}
impl Default for ResampleConfig {
fn default() -> Self {
Self {
out_rate: 16_000,
filter_half_width: 64,
}
}
}
pub fn resample(input: &Audio, config: ResampleConfig) -> Result<Audio> {
if config.out_rate == 0 || !(1..=1_024).contains(&config.filter_half_width) {
return Err(Error::invalid_argument(
"audio::resample requires a nonzero rate and filter_half_width in [1, 1024]",
));
}
let divisor = gcd(input.sample_rate(), config.out_rate);
let in_rate = input.sample_rate() / divisor;
let out_rate = config.out_rate / divisor;
let out_samples = input
.samples()
.checked_mul(out_rate as usize)
.and_then(|value| value.checked_div(in_rate as usize))
.ok_or_else(|| Error::out_of_range("audio::resample output length exceeds usize"))?;
if out_samples == 0 {
return Err(Error::invalid_argument(
"audio::resample output would be empty",
));
}
let channels = u32_extent(input.channels(), "audio::resample channels")?;
let in_samples = u32_extent(input.samples(), "audio::resample input samples")?;
let out_samples_u32 = u32_extent(out_samples, "audio::resample output samples")?;
let count = channels
.checked_mul(out_samples_u32)
.ok_or_else(|| Error::out_of_range("audio::resample dispatch exceeds u32"))?;
let matrix = Matrix::allocate(
input.engine_handle(),
vec![input.channels(), out_samples],
count as usize,
crate::DType::F32,
)?;
let output = Audio::new(matrix, config.out_rate, input.layout())?;
let push = [
PushConstant::U32(count),
PushConstant::U32(in_rate),
PushConstant::U32(out_rate),
PushConstant::U32(config.filter_half_width),
PushConstant::U32(in_samples),
PushConstant::U32(out_samples_u32),
];
let attributes = [
unsigned_attribute("out_rate", u64::from(config.out_rate)),
unsigned_attribute("filter_half_width", u64::from(config.filter_half_width)),
];
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioResampleF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioResampleF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::RESAMPLE,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
pub fn gain(input: &Audio, gain_db: f32) -> Result<Audio> {
if !gain_db.is_finite() || !(-300.0..=100.0).contains(&gain_db) {
return Err(Error::invalid_argument(
"audio::gain gain_db must be finite and in [-300, 100]",
));
}
let count = element_count_u32(input, crate::core::operation::audio::GAIN.name())?;
let output = wrap_like(allocate_like(input)?, input)?;
let push = [PushConstant::U32(count), PushConstant::F32(gain_db)];
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioGainF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioGainF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::GAIN,
inputs: &[input],
outputs: &outputs,
attributes: &[float_attribute("gain_db", gain_db)],
},
)?;
Ok(output)
}
pub fn clip(input: &Audio, minimum: f32, maximum: f32) -> Result<Audio> {
if !minimum.is_finite() || !maximum.is_finite() || minimum > maximum {
return Err(Error::invalid_argument(
"audio::clip requires finite ordered bounds",
));
}
let count = element_count_u32(input, crate::core::operation::audio::CLIP.name())?;
let output = wrap_like(allocate_like(input)?, input)?;
let push = [
PushConstant::U32(count),
PushConstant::F32(minimum),
PushConstant::F32(maximum),
];
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioClipF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioClipF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::CLIP,
inputs: &[input],
outputs: &outputs,
attributes: &[
float_attribute("minimum", minimum),
float_attribute("maximum", maximum),
],
},
)?;
Ok(output)
}
pub fn saturate(input: &Audio, drive_db: f32, mix: f32) -> Result<Audio> {
if !drive_db.is_finite()
|| !(-60.0..=60.0).contains(&drive_db)
|| !mix.is_finite()
|| !(0.0..=1.0).contains(&mix)
{
return Err(Error::invalid_argument(
"audio::saturate requires drive_db in [-60, 60] and mix in [0, 1]",
));
}
let count = element_count_u32(input, crate::core::operation::audio::SATURATE.name())?;
let output = wrap_like(allocate_like(input)?, input)?;
let push = [
PushConstant::U32(count),
PushConstant::F32(drive_db),
PushConstant::F32(mix),
];
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioSaturateF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioSaturateF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::SATURATE,
inputs: &[input],
outputs: &outputs,
attributes: &[
float_attribute("drive_db", drive_db),
float_attribute("mix", mix),
],
},
)?;
Ok(output)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BiquadCoefficients {
pub b0: f32,
pub b1: f32,
pub b2: f32,
pub a1: f32,
pub a2: f32,
}
impl Default for BiquadCoefficients {
fn default() -> Self {
Self {
b0: 1.0,
b1: 0.0,
b2: 0.0,
a1: 0.0,
a2: 0.0,
}
}
}
pub fn biquad(input: &Audio, coefficients: BiquadCoefficients) -> Result<Audio> {
if !stable_biquad(coefficients) {
return Err(Error::invalid_argument(
"audio biquad filtering requires one to 64 finite stable a0-normalized sections",
));
}
let channels = u32_extent(input.channels(), "audio::biquad channels")?;
let samples = u32_extent(input.samples(), "audio::biquad samples")?;
let blocks = samples.div_ceil(BIQUAD_BLOCK_SIZE);
let tasks = channels
.checked_mul(blocks)
.ok_or_else(|| Error::out_of_range("audio::biquad workspace exceeds u32"))?;
let summary_elements = usize::try_from(tasks)
.ok()
.and_then(|value| value.checked_mul(6))
.ok_or_else(|| Error::out_of_range("audio::biquad summary exceeds usize"))?;
let state_elements = usize::try_from(tasks)
.ok()
.and_then(|value| value.checked_mul(2))
.ok_or_else(|| Error::out_of_range("audio::biquad state exceeds usize"))?;
let summaries = Matrix::allocate(
input.engine_handle(),
vec![input.channels(), blocks as usize, 6],
summary_elements,
DType::F32,
)?;
let states = Matrix::allocate(
input.engine_handle(),
vec![input.channels(), blocks as usize, 2],
state_elements,
DType::F32,
)?;
let output_matrix = Matrix::allocate(
input.engine_handle(),
input.as_matrix().shape().to_vec(),
input.as_matrix().num_elements(),
DType::F32,
)?;
let output = Audio::new(output_matrix, input.sample_rate(), input.layout())?;
let filter_push = [
PushConstant::U32(channels),
PushConstant::U32(samples),
PushConstant::U32(blocks),
PushConstant::U32(BIQUAD_BLOCK_SIZE),
PushConstant::F32(coefficients.b0),
PushConstant::F32(coefficients.b1),
PushConstant::F32(coefficients.b2),
PushConstant::F32(coefficients.a1),
PushConstant::F32(coefficients.a2),
];
let summary_buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(summaries.storage()),
];
let scan_buffers = [
BufferBinding::read(summaries.storage()),
BufferBinding::write(states.storage()),
];
let scan_push = [PushConstant::U32(channels), PushConstant::U32(blocks)];
let apply_buffers = [
BufferBinding::read(input.storage()),
BufferBinding::read(states.storage()),
BufferBinding::write(output.storage()),
];
let dispatches = [
ComputeDispatch {
kernel: KernelId::AudioBiquadSummaryF32,
buffers: &summary_buffers,
push_constants: &filter_push,
workgroups: KernelId::AudioBiquadSummaryF32.linear_workgroups(tasks),
},
ComputeDispatch {
kernel: KernelId::AudioBiquadScanF32,
buffers: &scan_buffers,
push_constants: &scan_push,
workgroups: KernelId::AudioBiquadScanF32.linear_workgroups(channels),
},
ComputeDispatch {
kernel: KernelId::AudioBiquadApplyF32,
buffers: &apply_buffers,
push_constants: &filter_push,
workgroups: KernelId::AudioBiquadApplyF32.linear_workgroups(tasks),
},
];
let attributes = [
float_attribute("b0", coefficients.b0),
float_attribute("b1", coefficients.b1),
float_attribute("b2", coefficients.b2),
float_attribute("a1", coefficients.a1),
float_attribute("a2", coefficients.a2),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_split_semantic(
&dispatches,
AudioSemanticDispatch {
contract: crate::core::operation::audio::BIQUAD,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
pub fn reverb(input: &Audio, decay_seconds: f32, wet: f32) -> Result<Audio> {
if !decay_seconds.is_finite()
|| !(0.1..=10.0).contains(&decay_seconds)
|| !wet.is_finite()
|| !(0.0..=1.0).contains(&wet)
{
return Err(Error::invalid_argument(
"audio::reverb requires decay_seconds in [0.1, 10] and wet in [0, 1]",
));
}
let channels = u32_extent(input.channels(), "audio::reverb channels")?;
let in_samples = u32_extent(input.samples(), "audio::reverb input samples")?;
let tail = (f64::from(input.sample_rate()) * f64::from(decay_seconds)).ceil();
if !(1.0..=f64::from(u32::MAX)).contains(&tail) {
return Err(Error::out_of_range("audio::reverb tail exceeds u32"));
}
let tail = tail as u32;
let out_samples = in_samples
.checked_add(tail)
.ok_or_else(|| Error::out_of_range("audio::reverb output samples exceed u32"))?;
let count = channels
.checked_mul(out_samples)
.ok_or_else(|| Error::out_of_range("audio::reverb output dispatch exceeds u32"))?;
let output_shape = [input.channels(), out_samples as usize];
let allocate_output = || {
Matrix::allocate(
input.engine_handle(),
output_shape.to_vec(),
count as usize,
DType::F32,
)
};
let combs = [
allocate_output()?,
allocate_output()?,
allocate_output()?,
allocate_output()?,
];
let summed = allocate_output()?;
let diffuse_a = allocate_output()?;
let diffuse_b = allocate_output()?;
let output = Audio::new(allocate_output()?, input.sample_rate(), input.layout())?;
let delay_seconds = [0.0297_f64, 0.0371, 0.0411, 0.0437];
let mut delays = [0_u32; 4];
let mut feedback = [0_f32; 4];
let mut denominator = 0.0_f64;
for index in 0..4 {
delays[index] = (delay_seconds[index] * f64::from(input.sample_rate()))
.round()
.max(1.0) as u32;
feedback[index] = 0.001_f64
.powf((f64::from(delays[index]) / f64::from(input.sample_rate())) / f64::from(decay_seconds))
as f32;
denominator += 1.0 / (1.0 - f64::from(feedback[index]));
}
let allpass_delays = [
(0.0050_f64 * f64::from(input.sample_rate()))
.round()
.max(1.0) as u32,
(0.0017_f64 * f64::from(input.sample_rate()))
.round()
.max(1.0) as u32,
];
for delay in delays.into_iter().chain(allpass_delays) {
channels
.checked_mul(delay)
.ok_or_else(|| Error::out_of_range("audio::reverb recurrence dispatch exceeds u32"))?;
}
let comb_buffers_0 = [
BufferBinding::read(input.storage()),
BufferBinding::write(combs[0].storage()),
];
let comb_buffers_1 = [
BufferBinding::read(input.storage()),
BufferBinding::write(combs[1].storage()),
];
let comb_buffers_2 = [
BufferBinding::read(input.storage()),
BufferBinding::write(combs[2].storage()),
];
let comb_buffers_3 = [
BufferBinding::read(input.storage()),
BufferBinding::write(combs[3].storage()),
];
let comb_push_0 = [
PushConstant::U32(channels),
PushConstant::U32(in_samples),
PushConstant::U32(out_samples),
PushConstant::U32(delays[0]),
PushConstant::F32(feedback[0]),
];
let comb_push_1 = [
PushConstant::U32(channels),
PushConstant::U32(in_samples),
PushConstant::U32(out_samples),
PushConstant::U32(delays[1]),
PushConstant::F32(feedback[1]),
];
let comb_push_2 = [
PushConstant::U32(channels),
PushConstant::U32(in_samples),
PushConstant::U32(out_samples),
PushConstant::U32(delays[2]),
PushConstant::F32(feedback[2]),
];
let comb_push_3 = [
PushConstant::U32(channels),
PushConstant::U32(in_samples),
PushConstant::U32(out_samples),
PushConstant::U32(delays[3]),
PushConstant::F32(feedback[3]),
];
let sum_buffers = [
BufferBinding::read(combs[0].storage()),
BufferBinding::read(combs[1].storage()),
BufferBinding::read(combs[2].storage()),
BufferBinding::read(combs[3].storage()),
BufferBinding::write(summed.storage()),
];
let sum_push = [
PushConstant::U32(count),
PushConstant::F32((1.0 / denominator) as f32),
];
let allpass_a_buffers = [
BufferBinding::read(summed.storage()),
BufferBinding::write(diffuse_a.storage()),
];
let allpass_b_buffers = [
BufferBinding::read(diffuse_a.storage()),
BufferBinding::write(diffuse_b.storage()),
];
let allpass_a_push = [
PushConstant::U32(channels),
PushConstant::U32(out_samples),
PushConstant::U32(allpass_delays[0]),
PushConstant::F32(0.7),
];
let allpass_b_push = [
PushConstant::U32(channels),
PushConstant::U32(out_samples),
PushConstant::U32(allpass_delays[1]),
PushConstant::F32(0.7),
];
let mix_buffers = [
BufferBinding::read(input.storage()),
BufferBinding::read(diffuse_b.storage()),
BufferBinding::write(output.storage()),
];
let mix_push = [
PushConstant::U32(channels),
PushConstant::U32(in_samples),
PushConstant::U32(out_samples),
PushConstant::F32(wet),
];
let dispatches = [
ComputeDispatch {
kernel: KernelId::AudioReverbCombF32,
buffers: &comb_buffers_0,
push_constants: &comb_push_0,
workgroups: KernelId::AudioReverbCombF32.linear_workgroups(channels * delays[0]),
},
ComputeDispatch {
kernel: KernelId::AudioReverbCombF32,
buffers: &comb_buffers_1,
push_constants: &comb_push_1,
workgroups: KernelId::AudioReverbCombF32.linear_workgroups(channels * delays[1]),
},
ComputeDispatch {
kernel: KernelId::AudioReverbCombF32,
buffers: &comb_buffers_2,
push_constants: &comb_push_2,
workgroups: KernelId::AudioReverbCombF32.linear_workgroups(channels * delays[2]),
},
ComputeDispatch {
kernel: KernelId::AudioReverbCombF32,
buffers: &comb_buffers_3,
push_constants: &comb_push_3,
workgroups: KernelId::AudioReverbCombF32.linear_workgroups(channels * delays[3]),
},
ComputeDispatch {
kernel: KernelId::AudioReverbSumF32,
buffers: &sum_buffers,
push_constants: &sum_push,
workgroups: KernelId::AudioReverbSumF32.linear_workgroups(count),
},
ComputeDispatch {
kernel: KernelId::AudioReverbAllpassF32,
buffers: &allpass_a_buffers,
push_constants: &allpass_a_push,
workgroups: KernelId::AudioReverbAllpassF32.linear_workgroups(channels * allpass_delays[0]),
},
ComputeDispatch {
kernel: KernelId::AudioReverbAllpassF32,
buffers: &allpass_b_buffers,
push_constants: &allpass_b_push,
workgroups: KernelId::AudioReverbAllpassF32.linear_workgroups(channels * allpass_delays[1]),
},
ComputeDispatch {
kernel: KernelId::AudioReverbMixF32,
buffers: &mix_buffers,
push_constants: &mix_push,
workgroups: KernelId::AudioReverbMixF32.linear_workgroups(count),
},
];
let attributes = [
float_attribute("decay_seconds", decay_seconds),
float_attribute("wet", wet),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_split_semantic(
&dispatches,
AudioSemanticDispatch {
contract: crate::core::operation::audio::REVERB,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
pub fn sos_filter(input: &Audio, sections: &[BiquadCoefficients]) -> Result<Audio> {
if sections.is_empty() || sections.len() > 64 || !sections.iter().copied().all(stable_biquad) {
return Err(Error::invalid_argument(
"audio biquad filtering requires one to 64 finite stable a0-normalized sections",
));
}
let channels = u32_extent(input.channels(), "audio::sos_filter channels")?;
let samples = u32_extent(input.samples(), "audio::sos_filter samples")?;
let section_count = u32::try_from(sections.len())
.map_err(|_| Error::out_of_range("audio::sos_filter section count exceeds u32"))?;
let mut packed = Vec::new();
packed
.try_reserve_exact(sections.len() * 5)
.map_err(|_| Error::resource_exhausted("audio::sos_filter coefficient allocation failed"))?;
for section in sections {
packed.extend_from_slice(&[section.b0, section.b1, section.b2, section.a1, section.a2]);
}
let coefficient_matrix =
Matrix::from_slice_handle(input.engine_handle(), vec![sections.len(), 5], &packed)?;
let output_matrix = Matrix::allocate(
input.engine_handle(),
input.as_matrix().shape().to_vec(),
input.as_matrix().num_elements(),
DType::F32,
)?;
let output = Audio::new(output_matrix, input.sample_rate(), input.layout())?;
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::read(coefficient_matrix.storage()),
BufferBinding::write(output.storage()),
];
let push = [
PushConstant::U32(channels),
PushConstant::U32(samples),
PushConstant::U32(section_count),
];
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for value in packed {
for byte in value.to_bits().to_le_bytes() {
hash = (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3);
}
}
let attributes = [
unsigned_attribute("section_count", u64::from(section_count)),
unsigned_attribute("coefficient_hash", hash),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioSosFilterF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioSosFilterF32.linear_workgroups(channels),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::SOS_FILTER,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
pub fn pre_emphasis(input: &Audio, alpha: f32) -> Result<Audio> {
if !alpha.is_finite() {
return Err(Error::invalid_argument(
"audio::pre_emphasis alpha must be finite",
));
}
let samples = u32_extent(input.samples(), "audio::pre_emphasis samples")?;
let count = element_count_u32(input, crate::core::operation::audio::PRE_EMPHASIS.name())?;
let output = wrap_like(allocate_like(input)?, input)?;
let push = [
PushConstant::U32(count),
PushConstant::U32(samples),
PushConstant::F32(alpha),
];
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioPreEmphasisF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioPreEmphasisF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::PRE_EMPHASIS,
inputs: &[input],
outputs: &outputs,
attributes: &[float_attribute("alpha", alpha)],
},
)?;
Ok(output)
}
pub fn to_mono(input: &Audio) -> Result<Audio> {
let channels = u32_extent(input.channels(), "audio::to_mono channels")?;
let samples = u32_extent(input.samples(), "audio::to_mono samples")?;
let matrix = Matrix::allocate(
input.engine_handle(),
vec![1, input.samples()],
input.samples(),
crate::DType::F32,
)?;
let output = Audio::new(matrix, input.sample_rate(), AudioChannelLayout::Mono)?;
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let push = [PushConstant::U32(channels), PushConstant::U32(samples)];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioToMonoF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioToMonoF32.linear_workgroups(samples),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::TO_MONO,
inputs: &[input],
outputs: &outputs,
attributes: &[],
},
)?;
Ok(output)
}
pub fn fade(input: &Audio, fade_in_samples: u64, fade_out_samples: u64) -> Result<Audio> {
let samples = u32_extent(input.samples(), "audio::fade samples")?;
let fade_in = u32::try_from(fade_in_samples.min(u64::from(samples)))
.map_err(|_| Error::out_of_range("audio::fade fade-in exceeds u32"))?;
let fade_out = u32::try_from(fade_out_samples.min(u64::from(samples)))
.map_err(|_| Error::out_of_range("audio::fade fade-out exceeds u32"))?;
let count = element_count_u32(input, crate::core::operation::audio::FADE.name())?;
let output = wrap_like(allocate_like(input)?, input)?;
let push = [
PushConstant::U32(count),
PushConstant::U32(samples),
PushConstant::U32(fade_in),
PushConstant::U32(fade_out),
];
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioFadeF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioFadeF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::FADE,
inputs: &[input],
outputs: &outputs,
attributes: &[
unsigned_attribute("fade_in_samples", fade_in_samples),
unsigned_attribute("fade_out_samples", fade_out_samples),
],
},
)?;
Ok(output)
}
pub fn mix(a: &Audio, b: &Audio, gain_a: f32, gain_b: f32) -> Result<Audio> {
if a.sample_rate() != b.sample_rate()
|| a.layout() != b.layout()
|| a.as_matrix().shape() != b.as_matrix().shape()
|| !a.engine_handle().same_as(b.engine_handle())
|| !gain_a.is_finite()
|| !gain_b.is_finite()
{
return Err(Error::invalid_argument(
"audio::mix requires matching ownership, rate, layout, and shape plus finite gains",
));
}
let count = element_count_u32(a, "audio::mix")?;
let output = wrap_like(allocate_like(a)?, a)?;
let buffers = [
BufferBinding::read(a.storage()),
BufferBinding::read(b.storage()),
BufferBinding::write(output.storage()),
];
let push = [
PushConstant::U32(count),
PushConstant::F32(gain_a),
PushConstant::F32(gain_b),
];
let attributes = [
float_attribute("gain_a", gain_a),
float_attribute("gain_b", gain_b),
];
let outputs = [AudioSemanticOutput::Audio(&output)];
a.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioMixF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioMixF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::MIX,
inputs: &[a, b],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
pub fn amplitude_to_db(input: &Audio, floor_db: f32) -> Result<Matrix> {
if !floor_db.is_finite() || !(-300.0..=0.0).contains(&floor_db) {
return Err(Error::invalid_argument(
"audio::amplitude_to_db floor_db must be finite and in [-300, 0]",
));
}
let count = element_count_u32(input, "audio::amplitude_to_db")?;
let output = allocate_like(input)?;
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let push = [PushConstant::U32(count), PushConstant::F32(floor_db)];
let outputs = [AudioSemanticOutput::Matrix(&output)];
let attributes = [float_attribute("floor_db", floor_db)];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioAmplitudeToDbF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioAmplitudeToDbF32.linear_workgroups(count),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::AMPLITUDE_TO_DB,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}
pub fn waveform_envelope(input: &Audio, bins: u32) -> Result<Matrix> {
if bins == 0 || bins > 65_536 {
return Err(Error::invalid_argument(
"audio::waveform_envelope bins must be in [1, 65536]",
));
}
let channels = u32_extent(input.channels(), "audio::waveform_envelope channels")?;
let samples = u32_extent(input.samples(), "audio::waveform_envelope samples")?;
let elements = usize::try_from(bins)
.ok()
.and_then(|value| value.checked_mul(2))
.ok_or_else(|| Error::out_of_range("audio::waveform_envelope output exceeds usize"))?;
let output = Matrix::allocate(
input.engine_handle(),
vec![bins as usize, 2],
elements,
crate::DType::F32,
)?;
let buffers = [
BufferBinding::read(input.storage()),
BufferBinding::write(output.storage()),
];
let push = [
PushConstant::U32(channels),
PushConstant::U32(samples),
PushConstant::U32(bins),
];
let outputs = [AudioSemanticOutput::Matrix(&output)];
let attributes = [unsigned_attribute("bins", u64::from(bins))];
input.engine_handle().record_audio_semantic(
ComputeDispatch {
kernel: KernelId::AudioWaveformEnvelopeF32,
buffers: &buffers,
push_constants: &push,
workgroups: KernelId::AudioWaveformEnvelopeF32.linear_workgroups(bins),
},
AudioSemanticDispatch {
contract: crate::core::operation::audio::WAVEFORM_ENVELOPE,
inputs: &[input],
outputs: &outputs,
attributes: &attributes,
},
)?;
Ok(output)
}