mirror of
https://github.com/aphrody-labs/soundcraft.git
synced 2026-10-13 19:26:53 +00:00
Merge pull request #208 from Jafz2001/perf/reuse-mix-buffers
perf: reuse realtime mix buffers
This commit is contained in:
2 files changed
+68
-28
No files matched your search
+44
-15
@@ -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
@@ -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};
|
||||
|
||||
Reference in new issue
Block a user