Merge pull request #208 from Jafz2001/perf/reuse-mix-buffers

perf: reuse realtime mix buffers
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-11 20:05:01 -04:00
commit 5243f9a2a8
2 files changed
+68 -28

No files matched your search

+44 -15
View File
@@ -313,6 +313,8 @@ pub struct MixEngine {
adopt: HashMap<(TrackId, usize), PreparedInstance>,
/// Instances `sync` took out of service, for the host to drop (see [`MixEngine::take_retired`]).
retired: Vec<Box<dyn Plugin>>,
/// Ownership workspace reused for parallel strip processing.
work: Vec<(usize, Strip)>,
}
/// What `sync` needs to fill a slot.
@@ -376,6 +378,7 @@ impl MixEngine {
external: false,
adopt: HashMap::new(),
retired: Vec::new(),
work: Vec::new(),
}
}
@@ -574,16 +577,17 @@ impl MixEngine {
}
let any_solo = s.tracks.iter().any(|t| t.mixer.solo && !t.inactive);
let order = std::mem::take(&mut self.order);
let live = |t: &&Track| !(t.kind == TrackKind::Master || t.inactive || t.kind == TrackKind::Vca);
let live = |t: &Track| !(t.kind == TrackKind::Master || t.inactive || t.kind == TrackKind::Vca);
let is_aux = |t: &Track| matches!(t.kind, TrackKind::Aux | TrackKind::Folder);
// Phase 1: independent strips (audio, instrument, MIDI) in parallel.
let mut work: Vec<(&Track, Strip)> = order
.iter()
.filter_map(|&i| s.tracks.get(i))
.filter(live)
.filter(|t| !is_aux(t))
.filter_map(|t| self.strips.remove(&t.id).map(|st| (t, st)))
.collect();
let mut work = std::mem::take(&mut self.work);
work.clear();
for &i in &order {
let Some(t) = s.tracks.get(i).filter(|t| live(t) && !is_aux(t)) else { continue };
if let Some(st) = self.strips.remove(&t.id) {
work.push((i, st));
}
}
for (_, st) in work.iter_mut() {
if st.scratch.len() < self.max_block {
st.scratch.resize(self.max_block, 0.0);
@@ -591,7 +595,11 @@ impl MixEngine {
}
let busses = std::mem::take(&mut self.busses);
let live_in = LiveInput { input: &self.input, monitor_only: self.monitor_only, recording: self.recording };
run_strips(&mut work, |(t, st)| process_strip(s, t, st, &busses, &live_in, pos, frames, any_solo));
run_strips(&mut work, |(i, st)| {
if let Some(t) = s.tracks.get(*i) {
process_strip(s, t, st, &busses, &live_in, pos, frames, any_solo);
}
});
self.busses = busses;
// Delay compensation: align every strip to the slowest. Direct-to-main outputs also wait
// for the slowest aux return. Aux latency has to be read before those outputs are routed,
@@ -609,7 +617,8 @@ impl MixEngine {
if pdc { self.strips.iter().filter(|(id, _)| s.track(**id).is_some_and(is_aux)).map(|(_, st)| st.latency).max().unwrap_or(0) } else { 0 };
self.track_latency = lmax_t;
self.latency = lmax_t + lmax_a;
for (t, st) in work.iter_mut() {
for (i, st) in work.iter_mut() {
let Some(t) = s.tracks.get(*i) else { continue };
let ch = st.buf.len();
st.align.set(lmax_t.saturating_sub(st.latency), ch);
st.align.process(&mut st.buf, frames);
@@ -619,15 +628,20 @@ impl MixEngine {
}
st.out_extra.set(if t.mixer.output == Route::Main { lmax_a } else { 0 }, ch);
}
for (t, st) in work.iter_mut() {
self.route_strip(t, st, pos, frames);
for (i, st) in work.iter_mut() {
if let Some(t) = s.tracks.get(*i) {
self.route_strip(t, st, pos, frames);
}
}
for (t, st) in work {
self.strips.insert(t.id, st);
for (i, st) in work.drain(..) {
if let Some(t) = s.tracks.get(i) {
self.strips.insert(t.id, st);
}
}
self.work = work;
let is_aux = |t: &Track| matches!(t.kind, TrackKind::Aux | TrackKind::Folder);
// Phase 2: auxes in dependency order (they read busses).
for t in order.iter().filter_map(|&i| s.tracks.get(i)).filter(live).filter(|t| is_aux(t)) {
for t in order.iter().filter_map(|&i| s.tracks.get(i)).filter(|t| live(t)).filter(|t| is_aux(t)) {
let Some(mut st) = self.strips.remove(&t.id) else { continue };
if st.scratch.len() < self.max_block {
st.scratch.resize(self.max_block, 0.0);
@@ -2238,6 +2252,21 @@ mod tests {
assert!(out.iter().all(|c| c.iter().all(|x| *x == 0.0)));
}
#[test]
fn strip_workspace_keeps_its_allocation_between_blocks() {
let mut s = Session::default();
for _ in 0..6 {
s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
}
let mut eng = MixEngine::new(48_000.0, 512);
let mut out = vec![vec![0.0; 512]; 2];
eng.render(&s, 0, 512, &mut out);
let capacity = eng.work.capacity();
assert!(capacity >= 6);
eng.render(&s, 512, 512, &mut out);
assert_eq!(eng.work.capacity(), capacity);
}
#[test]
fn stopped_monitoring_does_not_click() {
let mut s = Session::default();
+24 -13
View File
@@ -86,7 +86,6 @@ struct AudioState {
playing: bool,
end: Option<Samples>,
looped: Option<Range>,
block: Vec<Vec<f32>>,
/// Rendered frames waiting to be consumed (planar, main channels) and the fractional read index.
pending: Vec<Vec<f32>>,
/// Stereo fold-down gains per main channel (used when the device has fewer channels).
@@ -122,7 +121,6 @@ impl AudioState {
playing: false,
end: None,
looped: None,
block: vec![vec![0.0; BLOCK]; nch],
pending: vec![vec![0.0; BLOCK]; nch],
pending_len: 0,
read: 0.0,
@@ -151,8 +149,7 @@ impl AudioState {
self.mix.adopt(instances);
// The main format changed (a document edit, not steady state): resize.
let nch = main_channels(&s);
if self.block.len() != nch {
self.block = vec![vec![0.0; BLOCK]; nch];
if self.pending.len() != nch {
self.pending = vec![vec![0.0; BLOCK]; nch];
self.pending_len = 0;
self.read = 0.0;
@@ -197,8 +194,8 @@ impl AudioState {
}
/// Render the next block at the session rate into `pending`.
fn render_next(&mut self) {
let mut n = BLOCK;
fn render_next(&mut self, quantum: usize) {
let mut n = quantum.clamp(1, BLOCK);
if let Some(l) = self.looped
&& self.pos >= l.end
{
@@ -231,13 +228,8 @@ impl AudioState {
}
self.mix.monitor_only = !self.playing;
self.mix.recording = self.recording;
self.mix.render(&self.session, self.pos, n, &mut self.block);
self.mix.render(&self.session, self.pos, n, &mut self.pending);
self.send_garbage();
for (d, s) in self.pending.iter_mut().zip(self.block.iter()) {
if let (Some(dd), Some(ss)) = (d.get_mut(..n), s.get(..n)) {
dd.copy_from_slice(ss);
}
}
self.pending_len = n;
if !self.playing {
return;
@@ -272,13 +264,15 @@ impl AudioState {
}
let speed = f64::from(self.shared.speed.load(Ordering::Relaxed)) / 1000.0;
let step = self.session.sample_rate.as_f64() / self.device_rate.max(1.0) * speed.clamp(0.1, 4.0);
let callback_frames = out.len() / channels;
let quantum = ((callback_frames as f64 * step).ceil() as usize).clamp(1, BLOCK);
for frame in out.chunks_mut(channels) {
while self.read >= self.pending_len as f64 {
self.read -= self.pending_len as f64;
if !self.playing && !monitoring {
break;
}
self.render_next();
self.render_next(quantum);
if self.pending_len == 0 {
break;
}
@@ -783,6 +777,23 @@ mod tests {
assert!(!shared.playing.load(Ordering::Relaxed));
}
#[test]
fn short_device_callbacks_render_short_mix_blocks() {
let s = Arc::new(soundcraft_model::Session::default());
let shared = Arc::new(Shared {
position: AtomicI64::new(0),
playing: AtomicBool::new(false),
speed: AtomicU32::new(1000),
meters: Mutex::new(MeterSnapshot::default()),
});
let (tx, rx) = channel();
let mut st = AudioState::new(rx, Arc::clone(&shared), s, 48_000.0, channel().0);
tx.send(Cmd::Play { from: 0, end: None, looped: None }).unwrap();
let mut out = vec![0.0f32; 2 * 64];
st.fill(&mut out, 2);
assert_eq!(shared.position.load(Ordering::Relaxed), 64);
}
#[test]
fn surround_main_maps_to_device_channels_or_folds_down() {
use soundcraft_model::{ChannelFormat, Clip, SourceAudio, SourceId, TrackKind};