diff --git a/crates/mix/src/lib.rs b/crates/mix/src/lib.rs index 7c9cb9d3..79c2fc84 100644 --- a/crates/mix/src/lib.rs +++ b/crates/mix/src/lib.rs @@ -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>, + /// 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(); diff --git a/crates/playback/src/lib.rs b/crates/playback/src/lib.rs index ccaf145f..cbd3b013 100644 --- a/crates/playback/src/lib.rs +++ b/crates/playback/src/lib.rs @@ -86,7 +86,6 @@ struct AudioState { playing: bool, end: Option, looped: Option, - block: Vec>, /// Rendered frames waiting to be consumed (planar, main channels) and the fractional read index. pending: Vec>, /// 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};