use cubecl::prelude::*;
use cubecl::server::Handle;
use super::engine::Pipeline;
use super::stage::PipelineStage;
use crate::core::{buffer, cast, cast_f32, DspFloat, EdgeMode};
use crate::filter::design::FilterError;
use crate::filter::fir::gaussian::GAUSSIAN_TRUNCATE;
use crate::filter::iir::DeviceFilter;
use crate::filter::{execute_fir_centered, execute_median, execute_teager_kaiser, gaussian_kernel_1d, FilterMode};
use crate::math::{execute_clamp, execute_scaling, execute_unpack_stored, stored_word_bytes, write_stored_owned};
use crate::spatial::{execute_common_median, execute_direct_car, DeviceSpatialMatrix};
use dspk_core::{DspError, DspResult, SampleFormat};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChunkMode {
Independent,
Stateful,
}
enum Planned<F: DspFloat> {
Stage(PipelineStage),
Filter { filter: DeviceFilter<F>, state: Handle },
SpatialMatrix(DeviceSpatialMatrix),
CenteredFir { taps: Handle, radius: usize, edge: EdgeMode },
}
pub struct PipelineWorkspace<F: DspFloat = f32> {
client: Client,
pipeline: Pipeline,
plan: Vec<Planned<F>>,
mode: ChunkMode,
started: bool,
channels: usize,
capacity_samples: usize,
buf_ping: Handle,
buf_pong: Handle,
scratch: Handle,
scratch_len: usize,
stored_scaling: Option<(Handle, Handle)>,
unpacked: Option<Handle>,
input: Option<Handle>,
stored_input: Option<(Handle, usize)>,
}
impl<F: DspFloat> PipelineWorkspace<F> {
pub fn new(
client: Client,
pipeline: Pipeline,
channels: usize,
initial_samples: usize,
sample_rate: f64,
) -> Result<Self, FilterError> {
Self::with_mode(client, pipeline, channels, initial_samples, sample_rate, ChunkMode::Independent)
}
pub fn new_stateful(
client: Client,
pipeline: Pipeline,
channels: usize,
initial_samples: usize,
sample_rate: f64,
) -> Result<Self, FilterError> {
Self::with_mode(client, pipeline, channels, initial_samples, sample_rate, ChunkMode::Stateful)
}
fn with_mode(
client: Client,
pipeline: Pipeline,
channels: usize,
initial_samples: usize,
sample_rate: f64,
mode: ChunkMode,
) -> Result<Self, FilterError> {
let mut plan = Vec::with_capacity(pipeline.len());
for stage in pipeline.stages() {
match stage {
PipelineStage::Filter(spec) => {
let filter = DeviceFilter::<F>::new(&client, spec, sample_rate)?;
if mode == ChunkMode::Stateful && filter.mode() != FilterMode::Forward {
return Err(FilterError::ForwardBackwardOnLiveStream);
}
let state = buffer::empty::<F>(&client, channels * filter.state_len());
plan.push(Planned::Filter { filter, state });
}
PipelineStage::SpatialWhitening(w) => {
assert_eq!(w.num_channels, channels, "SpatialWhitening channel mismatch");
plan.push(Planned::SpatialMatrix(DeviceSpatialMatrix::upload::<F>(&client, &w.matrix, channels)));
}
PipelineStage::SurfaceLaplacian(lap) => {
assert_eq!(lap.num_channels, channels, "SurfaceLaplacian channel mismatch");
plan.push(Planned::SpatialMatrix(DeviceSpatialMatrix::upload::<F>(&client, &lap.matrix, channels)));
}
PipelineStage::GaussianSmooth { sigma_samples, edge } => {
let (taps, radius) = gaussian_kernel_1d(*sigma_samples, GAUSSIAN_TRUNCATE);
plan.push(Planned::CenteredFir { taps: buffer::upload(&client, &cast_f32::<F>(&taps)), radius, edge: *edge });
}
other => plan.push(Planned::Stage(other.clone())),
}
}
let mut workspace = Self {
buf_ping: buffer::empty::<F>(&client, channels * initial_samples),
buf_pong: buffer::empty::<F>(&client, channels * initial_samples),
scratch: buffer::empty::<F>(&client, 1),
client,
pipeline,
plan,
mode,
started: false,
channels,
capacity_samples: initial_samples,
scratch_len: 1,
stored_scaling: None,
unpacked: None,
input: None,
stored_input: None,
};
workspace.reserve(initial_samples);
Ok(workspace)
}
#[inline]
pub fn client(&self) -> &Client {
&self.client
}
#[inline]
pub fn pipeline(&self) -> &Pipeline {
&self.pipeline
}
#[inline]
pub fn mode(&self) -> ChunkMode {
self.mode
}
pub fn reset(&mut self) {
self.started = false;
}
fn reserve(&mut self, samples: usize) {
if samples > self.capacity_samples {
self.buf_ping = buffer::empty::<F>(&self.client, self.channels * samples);
self.buf_pong = buffer::empty::<F>(&self.client, self.channels * samples);
self.unpacked = None;
self.input = None;
self.capacity_samples = samples;
}
let need = self
.plan
.iter()
.map(|p| match p {
Planned::Filter { filter, .. } => filter.scratch_len(self.channels, samples),
Planned::Stage(_) | Planned::SpatialMatrix(_) | Planned::CenteredFir { .. } => 0,
})
.max()
.unwrap_or(0);
if need > self.scratch_len {
self.scratch = buffer::empty::<F>(&self.client, need);
self.scratch_len = need;
}
}
pub fn process_handle(&mut self, input: &Handle, samples: usize) -> Handle {
if self.plan.is_empty() {
return input.clone();
}
self.reserve(samples);
let (channels, total) = (self.channels, self.channels * samples);
let first = !self.started;
self.started = true;
let mut current_in = input.clone();
let mut use_ping = true;
for planned in &self.plan {
let out = if use_ping { self.buf_ping.clone() } else { self.buf_pong.clone() };
let client = &self.client;
match planned {
Planned::Filter { filter, state } => match self.mode {
ChunkMode::Independent => {
filter.apply(client, ¤t_in, &out, &self.scratch, state, channels, samples)
}
ChunkMode::Stateful => filter
.apply_stateful(client, ¤t_in, &out, state, channels, samples, first)
.expect("stateful workspaces only hold forward filters"),
},
Planned::SpatialMatrix(matrix) => matrix.apply::<F>(client, ¤t_in, &out, channels, samples),
Planned::CenteredFir { taps, radius, edge } => {
execute_fir_centered::<F>(client, ¤t_in, &out, taps, channels, samples, *radius, *edge);
}
Planned::Stage(stage) => match stage {
PipelineStage::Scale { alpha, beta } => {
execute_scaling::<F>(client, ¤t_in, &out, total, cast(*alpha as f64), cast(*beta as f64))
}
PipelineStage::SubtractBaseline { baseline } => {
execute_scaling::<F>(client, ¤t_in, &out, total, cast(1.0), cast(-*baseline as f64))
}
PipelineStage::Clamp { min, max } => {
execute_clamp::<F>(client, ¤t_in, &out, total, cast(*min as f64), cast(*max as f64))
}
PipelineStage::CommonAverageReference => {
execute_direct_car::<F>(client, ¤t_in, &out, channels, samples)
}
PipelineStage::CommonMedianReference => {
execute_common_median::<F>(client, ¤t_in, &out, channels, samples)
}
PipelineStage::Median { width, edge } => {
execute_median::<F>(client, ¤t_in, &out, channels, samples, *width, *edge)
}
PipelineStage::TeagerKaiser { edge } => {
execute_teager_kaiser::<F>(client, ¤t_in, &out, channels, samples, *edge)
}
PipelineStage::Filter(_)
| PipelineStage::SpatialWhitening(_)
| PipelineStage::SurfaceLaplacian(_)
| PipelineStage::GaussianSmooth { .. } => {
unreachable!("pre-planned stage")
}
},
}
current_in = out;
use_ping = !use_ping;
}
buffer::truncate::<F>(current_in, (self.capacity_samples - samples) * channels)
}
pub fn process_chunk_in_vram(&mut self, input: &[F], samples: usize) -> Handle {
self.process_owned_chunk_in_vram(input.to_vec(), samples)
}
pub fn process_owned_chunk_in_vram(&mut self, input: Vec<F>, samples: usize) -> Handle {
assert_eq!(input.len(), self.channels * samples);
self.reserve(samples);
let capacity = self.channels * self.capacity_samples;
let len = input.len();
let in_handle = self.input.get_or_insert_with(|| buffer::empty::<F>(&self.client, capacity)).clone();
buffer::write_owned(&self.client, &in_handle, input);
let in_handle = buffer::truncate::<F>(in_handle, capacity - len);
self.process_handle(&in_handle, samples)
}
pub fn set_stored_scaling(&mut self, gains: &[f32], offsets: &[f32]) {
assert_eq!(gains.len(), self.channels);
assert_eq!(offsets.len(), self.channels);
self.stored_scaling =
Some((buffer::upload(&self.client, &cast_f32::<F>(gains)), buffer::upload(&self.client, &cast_f32::<F>(offsets))));
}
pub fn process_stored_chunk_in_vram(&mut self, stored: &[u8], format: SampleFormat, samples: usize) -> DspResult<Handle> {
self.process_owned_stored_chunk_in_vram(stored.to_vec(), format, samples)
}
pub fn process_owned_stored_chunk_in_vram(&mut self, stored: Vec<u8>, format: SampleFormat, samples: usize) -> DspResult<Handle> {
if stored.len() != self.channels * samples * format.bytes() {
return Err(DspError::ShapeMismatch { expected: vec![self.channels, samples, format.bytes()], actual: vec![stored.len()] });
}
let (gains, offsets) = self
.stored_scaling
.clone()
.ok_or_else(|| DspError::InvalidConfig("set_stored_scaling before processing stored chunks".into()))?;
self.reserve(samples);
let unpacked = self
.unpacked
.get_or_insert_with(|| buffer::empty::<F>(&self.client, self.channels * self.capacity_samples))
.clone();
let need = stored_word_bytes(stored.len());
let words = match &self.stored_input {
Some((handle, bytes)) if *bytes >= need => handle.clone(),
_ => {
let handle = self.client.empty(need);
self.stored_input = Some((handle.clone(), need));
handle
}
};
write_stored_owned(&self.client, &words, stored);
execute_unpack_stored::<F>(&self.client, &words, format, &gains, &offsets, &unpacked, self.channels, samples)?;
let unpacked = buffer::truncate::<F>(unpacked, (self.capacity_samples - samples) * self.channels);
Ok(self.process_handle(&unpacked, samples))
}
pub fn process_chunk(&mut self, input: &[F], samples: usize, output: &mut [F]) {
assert_eq!(input.len(), self.channels * samples);
assert_eq!(output.len(), self.channels * samples);
if self.plan.is_empty() {
output.copy_from_slice(input);
return;
}
let out_handle = self.process_chunk_in_vram(input, samples);
output.copy_from_slice(&buffer::download_prefix::<F>(&self.client, out_handle, output.len()));
}
}