use truce_params::{ParamFlags, ParamInfo, Params};
use crate::buffer::AudioBuffer;
use crate::events::{Event, EventBody, EventList, TransportInfo};
use crate::plugin::PluginRuntime;
use crate::process::{ProcessContext, ProcessStatus};
use crate::sample::Sample;
pub struct ChunkedProcess<'a> {
pub events: &'a EventList,
pub sub_event_scratch: &'a mut EventList,
pub transport: &'a mut TransportInfo,
pub sample_rate: f64,
pub output_events: &'a mut EventList,
pub params_fn: Option<&'a dyn Fn(u32) -> f64>,
pub meters_fn: Option<&'a dyn Fn(u32, f32)>,
pub param_infos: &'a [ParamInfo],
pub min_subblock_samples: u32,
}
pub fn process_chunked<S, P>(
plugin: &mut P,
params: &dyn Params,
buffer: &mut AudioBuffer<S>,
args: ChunkedProcess<'_>,
) -> ProcessStatus
where
S: Sample,
P: PluginRuntime<Sample = S>,
{
let ChunkedProcess {
events,
sub_event_scratch,
transport,
sample_rate,
output_events,
params_fn,
meters_fn,
param_infos,
min_subblock_samples,
} = args;
let total = buffer.num_samples();
let mut block_start = 0usize;
let mut event_idx = 0usize;
let mut last_status = ProcessStatus::Normal;
let min_sub = min_subblock_samples as usize;
while block_start < total {
let coalesce_until = block_start.saturating_add(min_sub).min(total);
let next_split = find_next_split(events, param_infos, event_idx, coalesce_until);
let block_end = next_split.map_or(total, |(s, _)| s.min(total));
apply_pending_events(events, params, transport, &mut event_idx, block_end);
rebase_events_into(events, sub_event_scratch, block_start, block_end);
let mut sub_buffer = buffer.slice(block_start, block_end - block_start);
let sub_output_start = output_events.len();
let mut ctx = ProcessContext::new(
transport,
sample_rate,
block_end - block_start,
output_events,
);
if let Some(f) = params_fn {
ctx = ctx.with_params(f);
}
if let Some(f) = meters_fn {
ctx = ctx.with_meters(f);
}
last_status = plugin.process(&mut sub_buffer, sub_event_scratch, &mut ctx);
rebase_output_events(output_events, sub_output_start, block_start);
block_start = block_end;
}
last_status
}
fn find_next_split(
events: &EventList,
param_infos: &[ParamInfo],
from: usize,
min_offset: usize,
) -> Option<(usize, usize)> {
for (i, ev) in events.iter().enumerate().skip(from) {
let offset = ev.sample_offset as usize;
if offset < min_offset {
continue;
}
if is_split_event(&ev.body, param_infos) {
return Some((offset, i));
}
}
None
}
fn is_split_event(body: &EventBody, param_infos: &[ParamInfo]) -> bool {
match body {
EventBody::ParamChange { id, .. }
| EventBody::ParamMod {
id, note_id: -1, ..
} => is_chunked(*id, param_infos),
EventBody::Transport(_) => true,
_ => false,
}
}
fn is_chunked(id: u32, param_infos: &[ParamInfo]) -> bool {
param_infos
.iter()
.find(|info| info.id == id)
.is_some_and(|info| info.flags.contains(ParamFlags::CHUNKED))
}
fn apply_pending_events(
events: &EventList,
params: &dyn Params,
transport: &mut TransportInfo,
event_idx: &mut usize,
block_end: usize,
) {
let mut i = *event_idx;
for ev in events.iter().skip(i) {
if (ev.sample_offset as usize) >= block_end {
break;
}
match ev.body {
EventBody::ParamChange { id, value } => {
params.set_plain(id, value);
}
EventBody::Transport(t) => {
*transport = t;
}
_ => {}
}
i += 1;
}
*event_idx = i;
}
fn rebase_events_into(
events: &EventList,
scratch: &mut EventList,
block_start: usize,
block_end: usize,
) {
scratch.clear();
for ev in events.iter() {
let off = ev.sample_offset as usize;
if off < block_start {
continue;
}
if off >= block_end {
break;
}
#[allow(clippy::cast_possible_truncation)]
let rebased_offset = (off - block_start) as u32;
scratch.push(Event {
sample_offset: rebased_offset,
body: ev.body,
});
}
}
fn rebase_output_events(output_events: &mut EventList, from: usize, sub_block_start: usize) {
#[allow(clippy::cast_possible_truncation)]
let shift = sub_block_start as u32;
if shift == 0 {
return;
}
let slice = output_events.events_mut();
for ev in slice.iter_mut().skip(from) {
ev.sample_offset = ev.sample_offset.saturating_add(shift);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::EVENT_LIST_PREALLOC;
use truce_params::{ParamFlags, ParamInfo, ParamRange, ParamUnit, ParamValueKind};
fn info(id: u32, chunked: bool) -> ParamInfo {
let flags = if chunked {
ParamFlags::AUTOMATABLE | ParamFlags::CHUNKED
} else {
ParamFlags::AUTOMATABLE
};
ParamInfo {
id,
name: "p",
short_name: "p",
group: "",
range: ParamRange::Linear { min: 0.0, max: 1.0 },
default_plain: 0.0,
flags,
unit: ParamUnit::None,
kind: ParamValueKind::Float,
}
}
#[test]
fn split_only_on_chunked_params() {
let infos = [info(0, true), info(1, false)];
let mut events = EventList::with_capacity(EVENT_LIST_PREALLOC);
events.push(Event {
sample_offset: 100,
body: EventBody::ParamChange { id: 1, value: 0.5 },
});
events.push(Event {
sample_offset: 200,
body: EventBody::ParamChange { id: 0, value: 0.5 },
});
let next = find_next_split(&events, &infos, 0, 0);
assert_eq!(next, Some((200, 1)));
}
#[test]
fn min_offset_skips_close_events() {
let infos = [info(0, true)];
let mut events = EventList::with_capacity(EVENT_LIST_PREALLOC);
events.push(Event {
sample_offset: 5,
body: EventBody::ParamChange { id: 0, value: 0.5 },
});
events.push(Event {
sample_offset: 50,
body: EventBody::ParamChange { id: 0, value: 0.6 },
});
let next = find_next_split(&events, &infos, 0, 32);
assert_eq!(next, Some((50, 1)));
}
#[test]
fn poly_mod_never_splits() {
let infos = [info(0, true)];
let mut events = EventList::with_capacity(EVENT_LIST_PREALLOC);
events.push(Event {
sample_offset: 100,
body: EventBody::ParamMod {
id: 0,
note_id: 7,
value: 0.1,
},
});
let next = find_next_split(&events, &infos, 0, 0);
assert_eq!(next, None);
}
#[test]
fn rebase_drops_out_of_window() {
let mut events = EventList::with_capacity(EVENT_LIST_PREALLOC);
events.push(Event {
sample_offset: 10,
body: EventBody::ParamChange { id: 0, value: 0.1 },
});
events.push(Event {
sample_offset: 50,
body: EventBody::ParamChange { id: 0, value: 0.2 },
});
events.push(Event {
sample_offset: 90,
body: EventBody::ParamChange { id: 0, value: 0.3 },
});
let mut scratch = EventList::with_capacity(EVENT_LIST_PREALLOC);
rebase_events_into(&events, &mut scratch, 40, 80);
let collected: Vec<u32> = scratch.iter().map(|e| e.sample_offset).collect();
assert_eq!(collected, vec![10]);
}
#[test]
fn transport_always_splits() {
let infos: [ParamInfo; 0] = [];
let mut events = EventList::with_capacity(EVENT_LIST_PREALLOC);
events.push(Event {
sample_offset: 100,
body: EventBody::Transport(TransportInfo::default()),
});
let next = find_next_split(&events, &infos, 0, 0);
assert_eq!(next, Some((100, 0)));
}
}