1use crate::{
2 executor::NodeJob,
3 message::{
4 Action, Message, OfflineAutomationLane, OfflineAutomationTarget, OfflineBounceWork,
5 ProcessTask,
6 },
7 midi::io::MidiEvent,
8 render_plan::Op,
9};
10#[cfg(unix)]
11use nix::libc;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tokio::sync::mpsc::{Receiver, Sender};
15
16pub(crate) struct NodeJobResult {
17 pub(crate) worker_id: usize,
18 pub(crate) epoch: u64,
19 pub(crate) node: u32,
20 pub(crate) output_linear: Vec<f32>,
21 pub(crate) parameter_updates: Vec<Action>,
22 pub(crate) latency_changed: bool,
23}
24
25#[derive(Debug)]
26pub struct Worker {
27 id: usize,
28 rx: Receiver<Message>,
29 tx: Sender<Message>,
30 realtime_priority: i32,
31}
32
33impl Worker {
34 fn apply_freeze_automation_at_sample(
35 track: &mut crate::track::TrackData,
36 sample: usize,
37 lanes: &[OfflineAutomationLane],
38 ) {
39 for lane in lanes {
40 if matches!(
41 lane.target,
42 OfflineAutomationTarget::Volume | OfflineAutomationTarget::Balance
43 ) {
44 continue;
45 }
46 let Some(value) = lane.value_at(sample) else {
47 continue;
48 };
49 match lane.target {
50 OfflineAutomationTarget::Volume | OfflineAutomationTarget::Balance => {}
51 OfflineAutomationTarget::MidiCc { channel, cc } => {
52 let cc_value = (value * 127.0).round() as u8;
53 track.rt.pending_automation_midi_events.push(MidiEvent::new(
54 0,
55 vec![0xB0 | channel.min(15), cc.min(127), cc_value],
56 ));
57 }
58 #[cfg(unix)]
59 OfflineAutomationTarget::Lv2Parameter {
60 instance_id,
61 index,
62 min,
63 max,
64 } => {
65 let lo = min.min(max);
66 let hi = max.max(min);
67 let param_value = (lo + value * (hi - lo)).clamp(lo, hi);
68 let _ = track.set_lv2_control_value(
69 instance_id,
70 index as usize,
71 param_value as f64,
72 );
73 }
74 OfflineAutomationTarget::Vst3Parameter {
75 instance_id,
76 param_id,
77 } => {
78 let _ = track.set_vst3_parameter(instance_id, param_id, value.clamp(0.0, 1.0));
79 }
80 OfflineAutomationTarget::ClapParameter {
81 instance_id,
82 param_id,
83 min,
84 max,
85 } => {
86 let lo = min.min(max);
87 let hi = max.max(min);
88 let param_value = (lo + value as f64 * (hi - lo)).clamp(lo, hi);
89 let _ = track.set_clap_parameter_at(instance_id, param_id, param_value, 0);
90 }
91 OfflineAutomationTarget::MixOsc { .. } => {
92 }
95 }
96 }
97 }
98
99 fn prepare_track_for_freeze_render(track: &mut crate::track::TrackData) -> (f32, f32) {
100 let original_level = track.level();
101 let original_balance = track.balance();
102 track.set_level(0.0);
103 track.set_balance(0.0);
104 (original_level, original_balance)
105 }
106
107 fn restore_track_after_freeze_render(
108 track: &mut crate::track::TrackData,
109 original_level: f32,
110 original_balance: f32,
111 ) {
112 track.set_level(original_level);
113 track.set_balance(original_balance);
114 }
115
116 async fn process_offline_bounce(&self, job: OfflineBounceWork) {
117 let track_handle = job.state.tracks.get(&job.track_name).cloned();
118 let Some(target_track) = track_handle else {
119 let _ = self
120 .tx
121 .send(Message::OfflineBounceFinished {
122 result: Err(format!("Track not found: {}", job.track_name)),
123 })
124 .await;
125 let _ = self.tx.send(Message::Ready(self.id)).await;
126 return;
127 };
128 let (channels, block_size, sample_rate) = {
129 let t = target_track.lock();
130 let block_size = t
131 .audio
132 .outs
133 .first()
134 .map(|io| io.buffer_size())
135 .or_else(|| t.audio.ins.first().map(|io| io.buffer_size()))
136 .unwrap_or(0)
137 .max(1);
138 (
139 t.audio.outs.len().max(1),
140 block_size,
141 t.sample_rate.round().max(1.0) as i32,
142 )
143 };
144 let freeze_state = if job.apply_fader {
145 None
146 } else {
147 let mut t = target_track.lock();
148 Some(Self::prepare_track_for_freeze_render(&mut t))
149 };
150
151 let all_tracks: Vec<_> = job.state.tracks.values().cloned().collect();
152 let plan_collector = basedrop::Collector::new();
153 let render_plan = crate::render_plan::RenderPlan::compile(&job.state, &[], &[], block_size);
154 let render_plan = Arc::new(basedrop::Owned::new(&plan_collector.handle(), render_plan));
155
156 let mut output_samples =
157 Vec::<f32>::with_capacity(job.length_samples.saturating_mul(channels.max(1)));
158
159 let mut cursor = 0usize;
160 let mut last_reported_progress = 0.0_f32;
161 let mut total_process_time = Duration::ZERO;
162 let mut total_write_time = Duration::ZERO;
163 while cursor < job.length_samples {
164 if job.cancel.load(std::sync::atomic::Ordering::Relaxed) {
165 let _ = std::fs::remove_file(&job.output_path);
166 if let Some((original_level, original_balance)) = freeze_state {
167 let mut t = target_track.lock();
168 Self::restore_track_after_freeze_render(
169 &mut t,
170 original_level,
171 original_balance,
172 );
173 }
174 let _ = self
175 .tx
176 .send(Message::OfflineBounceFinished {
177 result: Ok(Action::TrackOfflineBounceCanceled {
178 track_name: job.track_name.clone(),
179 }),
180 })
181 .await;
182 let _ = self.tx.send(Message::Ready(self.id)).await;
183 return;
184 }
185
186 let step = (job.length_samples - cursor).min(block_size);
187 for handle in &all_tracks {
188 let mut t = handle.lock();
189 t.audio.set_finished(false);
190 t.audio.set_processing(false);
191 t.set_transport_sample(job.start_sample.saturating_add(cursor));
192 t.set_loop_config(false, None);
193 t.set_transport_timing(job.tempo_bpm, job.tsig_num, job.tsig_denom);
194 t.set_clip_playback_enabled(true);
195 t.set_record_tap_enabled(false);
196 }
197 {
198 let mut t = target_track.lock();
199 Self::apply_freeze_automation_at_sample(
200 &mut t,
201 job.start_sample.saturating_add(cursor),
202 &job.automation_lanes,
203 );
204 }
205
206 let p_start = Instant::now();
207 for node in 0..render_plan.nodes.len() as crate::render_plan::NodeId {
208 let _ = Self::process_node_job_result(
209 self.id,
210 NodeJob {
211 epoch: 0,
212 plan: render_plan.clone(),
213 node,
214 },
215 );
216 }
217 total_process_time += p_start.elapsed();
218
219 let write_start = Instant::now();
220 {
221 let t = target_track.lock();
222 let outs = t.last_audio_outputs();
223 for i in 0..step {
224 for ch in 0..channels {
225 let sample = outs
226 .get(ch)
227 .and_then(|out| out.get(i))
228 .copied()
229 .unwrap_or(0.0);
230 output_samples.push(sample);
231 }
232 }
233 }
234 total_write_time += write_start.elapsed();
235
236 cursor = cursor.saturating_add(step);
237 let progress = (cursor as f32 / job.length_samples as f32).clamp(0.0, 1.0);
238
239 if progress - last_reported_progress >= 0.01 || cursor >= job.length_samples {
240 last_reported_progress = progress;
241 let _ = self
242 .tx
243 .send(Message::OfflineBounceFinished {
244 result: Ok(Action::TrackOfflineBounceProgress {
245 track_name: job.track_name.clone(),
246 progress,
247 operation: Some("Rendering freeze".to_string()),
248 }),
249 })
250 .await;
251 }
252 }
253
254 if let Err(e) = crate::audio_codec::write_wav_f32(
255 std::path::Path::new(&job.output_path),
256 &output_samples,
257 channels,
258 sample_rate as u32,
259 ) {
260 let _ = std::fs::remove_file(&job.output_path);
261 if let Some((original_level, original_balance)) = freeze_state {
262 let mut t = target_track.lock();
263 Self::restore_track_after_freeze_render(&mut t, original_level, original_balance);
264 }
265 let _ = self
266 .tx
267 .send(Message::OfflineBounceFinished {
268 result: Err(format!(
269 "Failed to write offline bounce '{}': {e}",
270 job.output_path
271 )),
272 })
273 .await;
274 let _ = self.tx.send(Message::Ready(self.id)).await;
275 return;
276 }
277
278 if let Some((original_level, original_balance)) = freeze_state {
279 let mut t = target_track.lock();
280 Self::restore_track_after_freeze_render(&mut t, original_level, original_balance);
281 }
282
283 let _ = self
284 .tx
285 .send(Message::OfflineBounceFinished {
286 result: Ok(Action::TrackOfflineBounce {
287 track_name: job.track_name,
288 output_path: job.output_path,
289 start_sample: job.start_sample,
290 length_samples: job.length_samples,
291 automation_lanes: vec![],
292 apply_fader: job.apply_fader,
293 }),
294 })
295 .await;
296 let _ = self.tx.send(Message::Ready(self.id)).await;
297 }
298
299 #[cfg(unix)]
300 pub(crate) fn try_enable_realtime(priority: i32) -> Result<(), String> {
301 let thread = unsafe { libc::pthread_self() };
302 let policy = libc::SCHED_FIFO;
303 let param = unsafe {
304 let mut p = std::mem::zeroed::<libc::sched_param>();
305 p.sched_priority = priority;
306 p
307 };
308 let rc = unsafe { libc::pthread_setschedparam(thread, policy, ¶m) };
309 if rc == 0 {
310 Ok(())
311 } else {
312 Err(format!("pthread_setschedparam failed with errno {}", rc))
313 }
314 }
315
316 #[cfg(target_os = "windows")]
317 pub(crate) fn try_enable_realtime(_priority: i32) -> Result<(), String> {
318 use std::{cell::Cell, ffi::OsStr, os::windows::ffi::OsStrExt};
319
320 #[link(name = "avrt")]
321 unsafe extern "system" {
322 fn AvSetMmThreadCharacteristicsW(task_name: *const u16, task_index: *mut u32) -> isize;
323 }
324
325 thread_local! {
326 static MMCSS_TASK_HANDLE: Cell<isize> = const { Cell::new(0) };
327 }
328
329 MMCSS_TASK_HANDLE.with(|handle| {
330 if handle.get() != 0 {
331 return Ok(());
332 }
333
334 let task_name: Vec<u16> = OsStr::new("Pro Audio")
335 .encode_wide()
336 .chain(Some(0))
337 .collect();
338 let mut task_index = 0_u32;
339 let mmcss_handle =
340 unsafe { AvSetMmThreadCharacteristicsW(task_name.as_ptr(), &mut task_index) };
341 if mmcss_handle == 0 {
342 Err(format!(
343 "AvSetMmThreadCharacteristicsW(Pro Audio) failed: {}",
344 std::io::Error::last_os_error()
345 ))
346 } else {
347 handle.set(mmcss_handle);
348 Ok(())
349 }
350 })
351 }
352
353 #[cfg(all(not(unix), not(target_os = "windows")))]
354 pub(crate) fn try_enable_realtime(_priority: i32) -> Result<(), String> {
355 Err("Realtime thread priority is not supported on this platform".to_string())
356 }
357
358 pub async fn new(
359 id: usize,
360 rx: Receiver<Message>,
361 tx: Sender<Message>,
362 realtime_priority: i32,
363 ) -> Worker {
364 let worker = Worker {
365 id,
366 rx,
367 tx,
368 realtime_priority,
369 };
370 worker.send(Message::Ready(id)).await;
371 worker
372 }
373
374 pub async fn send(&self, message: Message) {
375 self.tx
376 .send(message)
377 .await
378 .expect("Failed to send message from worker");
379 }
380
381 fn arena_input_slices<'a>(
382 plan: &'a crate::render_plan::RenderPlan,
383 ins: &[crate::render_plan::BufferId],
384 ) -> Vec<&'a [f32]> {
385 ins.iter()
386 .map(|&buf| {
387 unsafe { plan.buffer(buf) }
390 })
391 .collect()
392 }
393
394 fn arena_source_slices<'a>(
395 plan: &'a crate::render_plan::RenderPlan,
396 writable: &[crate::render_plan::BufferId],
397 ) -> Vec<(usize, &'a [f32], usize)> {
398 plan.port_map
399 .iter()
400 .filter_map(|(&key, &buf)| {
401 if writable.contains(&buf) {
402 return None;
403 }
404 Some((key, unsafe { plan.buffer(buf) }, plan.buffer_latency(buf)))
409 })
410 .collect()
411 }
412
413 fn metronome_output_buffer(
414 plan: &crate::render_plan::RenderPlan,
415 t: &crate::track::TrackData,
416 outs: &[crate::render_plan::BufferId],
417 ) -> Option<crate::render_plan::BufferId> {
418 let source = t.metronome_source()?;
419 let key = Arc::as_ptr(&source) as usize;
420 let &buf = plan.port_map.get(&key)?;
421 outs.contains(&buf).then_some(buf)
422 }
423
424 pub(crate) fn process_node_job_result(worker_id: usize, job: NodeJob) -> NodeJobResult {
431 let NodeJob { epoch, plan, node } = job;
432 let (output_linear, parameter_updates, latency_changed) = match &plan.nodes[node as usize] {
433 Op::Zero { output } => {
434 unsafe { &mut *plan.buffer_ptr(*output) }.fill(0.0);
438 plan.set_buffer_latency(*output, 0);
439 (Vec::new(), Vec::new(), false)
440 }
441 Op::Sum {
442 inputs,
443 delays,
444 output,
445 } => {
446 let out = unsafe { &mut *plan.buffer_ptr(*output) };
449 out.fill(0.0);
450 let max_latency = inputs
451 .iter()
452 .map(|&input| plan.buffer_latency(input))
453 .max()
454 .unwrap_or(0);
455 for (idx, &input) in inputs.iter().enumerate() {
456 let src = unsafe { plan.buffer(input) };
457 let delay = max_latency.saturating_sub(plan.buffer_latency(input));
458 let line = unsafe { &mut *delays[idx].get() };
462 line.process(src, delay, out, idx != 0);
463 }
464 plan.set_buffer_latency(*output, max_latency);
465 (Vec::new(), Vec::new(), false)
466 }
467 Op::HwInput { output, .. } => {
468 plan.set_buffer_latency(*output, 0);
471 (Vec::new(), Vec::new(), false)
472 }
473 Op::Task { task, ins, outs } => {
474 let track = match task {
475 ProcessTask::Track(t)
476 | ProcessTask::FolderInput(t)
477 | ProcessTask::FolderOutput(t) => t,
478 ProcessTask::Plugin { track, .. } => track,
479 };
480 let mut t = track.lock();
481 t.audio.set_processing(true);
487 match task {
488 ProcessTask::Track(_) => {
489 let audio_out_count = t.audio.outs.len();
490 let metronome_output = Self::metronome_output_buffer(&plan, &t, outs);
491 let input_ptrs = ins
492 .iter()
493 .map(|&buf| {
494 unsafe { plan.buffer_ptr(buf) }
497 })
498 .collect::<Vec<_>>();
499 let mut inputs = input_ptrs
500 .iter()
501 .map(|&ptr| {
502 unsafe { (&mut *ptr).as_mut_slice() }
505 })
506 .collect::<Vec<_>>();
507 let source_buffers = Self::arena_source_slices(&plan, outs);
508 let output_ptrs = outs
509 .iter()
510 .take(audio_out_count)
511 .map(|&buf| {
512 unsafe { plan.buffer_ptr(buf) }
515 })
516 .collect::<Vec<_>>();
517 let mut outputs = output_ptrs
518 .iter()
519 .map(|&ptr| {
520 unsafe { (&mut *ptr).as_mut_slice() }
523 })
524 .collect::<Vec<_>>();
525 let metronome_output_ptr = metronome_output.map(|buf| {
526 unsafe { plan.buffer_ptr(buf) }
529 });
530 let metronome_output = metronome_output_ptr.map(|ptr| {
531 unsafe { (&mut *ptr).as_mut_slice() }
534 });
535 t.process_render_block_with_audio_buffers_and_metronome(
536 &mut inputs,
537 &mut outputs,
538 &source_buffers,
539 metronome_output,
540 );
541 for &out in outs.iter().take(audio_out_count) {
542 plan.set_buffer_latency(out, t.plugin_graph_latency_samples());
543 }
544 }
545 ProcessTask::FolderInput(_) => {
546 let metronome_output = Self::metronome_output_buffer(&plan, &t, outs);
547 let input_ptrs = ins
548 .iter()
549 .map(|&buf| {
550 unsafe { plan.buffer_ptr(buf) }
553 })
554 .collect::<Vec<_>>();
555 let mut inputs = input_ptrs
556 .iter()
557 .map(|&ptr| {
558 unsafe { (&mut *ptr).as_mut_slice() }
561 })
562 .collect::<Vec<_>>();
563 let metronome_output_ptr = metronome_output.map(|buf| {
564 unsafe { plan.buffer_ptr(buf) }
567 });
568 let metronome_output = metronome_output_ptr.map(|ptr| {
569 unsafe { (&mut *ptr).as_mut_slice() }
572 });
573 t.process_folder_input_with_audio_buffers_and_metronome(
574 &mut inputs,
575 metronome_output,
576 );
577 for &input in ins {
578 plan.set_buffer_latency(input, 0);
579 }
580 }
581 ProcessTask::FolderOutput(_) => {
582 let source_buffers = Self::arena_source_slices(&plan, outs);
583 let output_ptrs = outs
584 .iter()
585 .map(|&buf| {
586 unsafe { plan.buffer_ptr(buf) }
589 })
590 .collect::<Vec<_>>();
591 let mut outputs = output_ptrs
592 .iter()
593 .map(|&ptr| {
594 unsafe { (&mut *ptr).as_mut_slice() }
597 })
598 .collect::<Vec<_>>();
599 t.process_folder_output_with_audio_buffers(&mut outputs, &source_buffers);
600 for &out in outs {
601 plan.set_buffer_latency(out, t.plugin_graph_latency_samples());
602 }
603 }
604 ProcessTask::Plugin { kind, index, .. } => {
605 let input_latency = ins
606 .iter()
607 .map(|&input| plan.buffer_latency(input))
608 .max()
609 .unwrap_or(0);
610 let inputs = Self::arena_input_slices(&plan, ins);
611 let output_ptrs = outs
612 .iter()
613 .map(|&buf| {
614 unsafe { plan.buffer_ptr(buf) }
617 })
618 .collect::<Vec<_>>();
619 let mut outputs = output_ptrs
620 .iter()
621 .map(|&ptr| {
622 unsafe { (&mut *ptr).as_mut_slice() }
625 })
626 .collect::<Vec<_>>();
627 t.process_plugin_with_audio_buffers(*kind, *index, &inputs, &mut outputs);
628 let latency =
629 input_latency.saturating_add(t.plugin_latency_samples(*kind, *index));
630 for &out in outs {
631 plan.set_buffer_latency(out, latency);
632 }
633 }
634 }
635 t.audio.set_processing(false);
636 let latency_changed = t.take_plugin_latency_changed();
637 let updates = std::mem::take(&mut t.rt.echoed_parameter_updates);
638 let meter = t.output_meter_linear();
639 {
640 static TRACK_OUTPUT_PEAK_LOG_COUNT: std::sync::atomic::AtomicUsize =
641 std::sync::atomic::AtomicUsize::new(0);
642 let peak = meter.iter().copied().fold(0.0_f32, f32::max);
643 let count = TRACK_OUTPUT_PEAK_LOG_COUNT
644 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
645 if count < 64 || peak > 0.0 {
646 let task_kind = match task {
647 ProcessTask::Track(_) => "track",
648 ProcessTask::FolderInput(_) => "folder_input",
649 ProcessTask::FolderOutput(_) => "folder_output",
650 ProcessTask::Plugin { .. } => "plugin",
651 };
652 tracing::debug!(
653 worker_id,
654 node,
655 track = %t.name,
656 task_kind,
657 peak,
658 meter = ?meter,
659 "track output meter peak"
660 );
661 }
662 }
663 (meter, updates, latency_changed)
664 }
665 };
666 NodeJobResult {
667 worker_id,
668 epoch,
669 node,
670 output_linear,
671 parameter_updates,
672 latency_changed,
673 }
674 }
675
676 async fn process_node_job(&self, job: NodeJob) {
677 let result = Self::process_node_job_result(self.id, job);
678 let _ = self.tx.send(result.into()).await;
679 }
680
681 pub async fn work(&mut self) {
682 crate::enable_flush_denormals_to_zero();
683 if let Err(e) = Self::try_enable_realtime(self.realtime_priority) {
684 tracing::warn!(
685 "Worker {} realtime priority {} not enabled: {}",
686 self.id,
687 self.realtime_priority,
688 e
689 );
690 }
691 while let Some(message) = self.rx.recv().await {
692 match message {
693 Message::Request(Action::Quit) => {
694 return;
695 }
696 Message::ProcessOfflineBounce(job) => {
697 self.process_offline_bounce(job).await;
698 }
699 Message::NodeJob(job) => {
700 self.process_node_job(job).await;
701 }
702 _ => {}
703 }
704 }
705 }
706}
707
708impl From<NodeJobResult> for Message {
709 fn from(result: NodeJobResult) -> Self {
710 Message::NodeDone {
711 worker_id: result.worker_id,
712 epoch: result.epoch,
713 node: result.node,
714 output_linear: result.output_linear,
715 parameter_updates: result.parameter_updates,
716 latency_changed: result.latency_changed,
717 }
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::Worker;
724 use crate::message::{
725 Action, Message, OfflineAutomationLane, OfflineAutomationPoint, OfflineAutomationTarget,
726 OfflineBounceWork,
727 };
728 use crate::state::State;
729 use crate::track::Track;
730 use std::path::PathBuf;
731 use std::sync::{Arc, atomic::AtomicBool};
732 use std::time::{SystemTime, UNIX_EPOCH};
733 use tokio::sync::mpsc::channel;
734
735 fn make_state_with_track(track: Track) -> State {
736 let mut state = State::default();
737 state.tracks.insert(track.name.clone(), Arc::new(track));
738 state
739 }
740
741 fn unique_temp_wav(name: &str) -> PathBuf {
742 let nanos = SystemTime::now()
743 .duration_since(UNIX_EPOCH)
744 .expect("clock")
745 .as_nanos();
746 std::env::temp_dir().join(format!("maolan_{name}_{nanos}.wav"))
747 }
748
749 #[test]
750 fn prepare_track_for_freeze_render_neutralizes_level_and_balance() {
751 let mut track = Track::new("track".to_string(), 1, 2, 0, 0, 64, 48_000.0);
752 track.set_level(-6.0);
753 track.set_balance(0.35);
754
755 let (level, balance) = Worker::prepare_track_for_freeze_render(&mut track);
756
757 assert_eq!(level, -6.0);
758 assert_eq!(balance, 0.35);
759 assert_eq!(track.level(), 0.0);
760 assert_eq!(track.balance(), 0.0);
761
762 Worker::restore_track_after_freeze_render(&mut track, level, balance);
763 assert_eq!(track.level(), -6.0);
764 assert_eq!(track.balance(), 0.35);
765 }
766
767 #[test]
768 fn freeze_automation_ignores_volume_and_balance_lanes() {
769 let mut track = Track::new("track".to_string(), 1, 2, 0, 1, 64, 48_000.0);
770 let lanes = vec![
771 OfflineAutomationLane {
772 target: OfflineAutomationTarget::Volume,
773 visible: true,
774 points: vec![OfflineAutomationPoint {
775 sample: 0,
776 value: 0.0,
777 }],
778 },
779 OfflineAutomationLane {
780 target: OfflineAutomationTarget::Balance,
781 visible: true,
782 points: vec![OfflineAutomationPoint {
783 sample: 0,
784 value: 1.0,
785 }],
786 },
787 OfflineAutomationLane {
788 target: OfflineAutomationTarget::MidiCc { channel: 0, cc: 7 },
789 visible: true,
790 points: vec![OfflineAutomationPoint {
791 sample: 0,
792 value: 1.0,
793 }],
794 },
795 ];
796
797 Worker::apply_freeze_automation_at_sample(&mut track, 0, &lanes);
798
799 assert_eq!(track.level(), 0.0);
800 assert_eq!(track.balance(), 0.0);
801 assert_eq!(track.rt.pending_automation_midi_events.len(), 1);
802 assert_eq!(
803 track.rt.pending_automation_midi_events[0].data,
804 vec![0xB0, 7, 127]
805 );
806 }
807
808 #[test]
809 fn automation_lane_value_at_interpolates_between_points() {
810 let lane = OfflineAutomationLane {
811 target: OfflineAutomationTarget::Volume,
812 visible: true,
813 points: vec![
814 OfflineAutomationPoint {
815 sample: 10,
816 value: 0.25,
817 },
818 OfflineAutomationPoint {
819 sample: 20,
820 value: 0.75,
821 },
822 ],
823 };
824 let value = lane.value_at(15).expect("value");
825
826 assert!((value - 0.5).abs() < 1.0e-6);
827 }
828
829 #[test]
830 fn freeze_automation_applies_interpolated_midi_cc_lane() {
831 let mut track = Track::new("track".to_string(), 1, 1, 0, 1, 64, 48_000.0);
832 let lanes = vec![OfflineAutomationLane {
833 target: OfflineAutomationTarget::MidiCc { channel: 0, cc: 7 },
834 visible: true,
835 points: vec![
836 OfflineAutomationPoint {
837 sample: 0,
838 value: 0.0,
839 },
840 OfflineAutomationPoint {
841 sample: 10,
842 value: 1.0,
843 },
844 ],
845 }];
846
847 Worker::apply_freeze_automation_at_sample(&mut track, 5, &lanes);
848 assert_eq!(track.rt.pending_automation_midi_events.len(), 1);
849 assert_eq!(track.rt.pending_automation_midi_events[0].data[2], 64);
850
851 track.rt.pending_automation_midi_events.clear();
852 Worker::apply_freeze_automation_at_sample(&mut track, 2, &lanes);
853 assert_eq!(track.rt.pending_automation_midi_events.len(), 1);
854 assert_eq!(track.rt.pending_automation_midi_events[0].data[2], 25);
855 }
856
857 #[cfg_attr(
858 all(miri, target_os = "freebsd"),
859 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
860 )]
861 #[tokio::test]
862 async fn process_node_job_sums_arena_buffers() {
863 use crate::render_plan::{Op, RenderPlan};
864 use std::cell::UnsafeCell;
865 use std::collections::HashMap;
866
867 let (_rx_unused_tx, rx_unused) = channel(1);
868 let (tx, mut out_rx) = channel(8);
869 let worker = Worker {
870 id: 4,
871 rx: rx_unused,
872 tx,
873 realtime_priority: 0,
874 };
875 let collector = basedrop::Collector::new();
876 let plan = RenderPlan {
877 buffer_size: 4,
878 buffers: (0..3).map(|_| UnsafeCell::new(vec![0.0; 4])).collect(),
879 buffer_latencies: (0..3)
880 .map(|_| std::sync::atomic::AtomicUsize::new(0))
881 .collect(),
882 nodes: vec![Op::Sum {
883 inputs: vec![0, 1],
884 delays: vec![
885 UnsafeCell::new(crate::render_plan::DelayLine::new()),
886 UnsafeCell::new(crate::render_plan::DelayLine::new()),
887 ],
888 output: 2,
889 }],
890 indegree: vec![0],
891 dependents: vec![vec![]],
892 sources: vec![0],
893 hw_in_map: vec![],
894 hw_out_map: vec![],
895 port_map: HashMap::new(),
896 midi_edges: vec![],
897 forced: vec![],
898 };
899 unsafe {
901 (&mut *plan.buffer_ptr(0)).copy_from_slice(&[0.25, 0.5, 0.75, 1.0]);
902 (&mut *plan.buffer_ptr(1)).copy_from_slice(&[0.75, 0.5, 0.25, f32::NAN]);
903 }
904 let shared = std::sync::Arc::new(basedrop::Owned::new(&collector.handle(), plan));
905
906 worker
907 .process_node_job(crate::executor::NodeJob {
908 epoch: 1,
909 plan: shared.clone(),
910 node: 0,
911 })
912 .await;
913
914 unsafe {
917 assert_eq!(
918 &*shared.buffer_ptr(2),
919 &vec![1.0, 1.0, 1.0, 1.0],
920 "sanitized sum in the arena"
921 );
922 }
923 match out_rx.recv().await.expect("message") {
924 Message::NodeDone {
925 worker_id,
926 epoch,
927 node,
928 output_linear,
929 ..
930 } => {
931 assert_eq!(worker_id, 4);
932 assert_eq!(epoch, 1);
933 assert_eq!(node, 0);
934 assert!(output_linear.is_empty());
935 }
936 other => panic!("unexpected message: {other:?}"),
937 }
938 }
939
940 #[cfg_attr(
941 all(miri, target_os = "freebsd"),
942 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
943 )]
944 #[tokio::test]
945 async fn process_offline_bounce_errors_when_track_is_missing() {
946 let (_rx_unused_tx, rx_unused) = channel(1);
947 let (tx, mut out_rx) = channel(8);
948 let worker = Worker {
949 id: 7,
950 rx: rx_unused,
951 tx,
952 realtime_priority: 0,
953 };
954 let job = OfflineBounceWork {
955 state: Arc::new(State::default().snapshot()),
956 track_name: "missing".to_string(),
957 output_path: unique_temp_wav("missing").to_string_lossy().to_string(),
958 start_sample: 0,
959 length_samples: 8,
960 tempo_bpm: 120.0,
961 tsig_num: 4,
962 tsig_denom: 4,
963 automation_lanes: vec![],
964 cancel: Arc::new(AtomicBool::new(false)),
965 apply_fader: false,
966 };
967
968 worker.process_offline_bounce(job).await;
969
970 match out_rx.recv().await.expect("message") {
971 Message::OfflineBounceFinished { result: Err(err) } => {
972 assert!(err.contains("Track not found: missing"));
973 }
974 other => panic!("unexpected message: {other:?}"),
975 }
976 }
977
978 #[cfg_attr(
979 all(miri, target_os = "freebsd"),
980 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
981 )]
982 #[tokio::test]
983 async fn process_offline_bounce_cancels_and_restores_track_state() {
984 let (_rx_unused_tx, rx_unused) = channel(1);
985 let (tx, mut out_rx) = channel(8);
986 let worker = Worker {
987 id: 5,
988 rx: rx_unused,
989 tx,
990 realtime_priority: 0,
991 };
992 let track = Track::new("track".to_string(), 1, 2, 0, 0, 4, 48_000.0);
993 track.set_level(-9.0);
994 track.set_balance(-0.3);
995 let state = make_state_with_track(track);
996 let job = OfflineBounceWork {
997 state: Arc::new(state.lock().snapshot()),
998 track_name: "track".to_string(),
999 output_path: unique_temp_wav("cancel").to_string_lossy().to_string(),
1000 start_sample: 0,
1001 length_samples: 8,
1002 tempo_bpm: 120.0,
1003 tsig_num: 4,
1004 tsig_denom: 4,
1005 automation_lanes: vec![],
1006 cancel: Arc::new(AtomicBool::new(true)),
1007 apply_fader: false,
1008 };
1009
1010 worker.process_offline_bounce(job).await;
1011
1012 match out_rx.recv().await.expect("message") {
1013 Message::OfflineBounceFinished {
1014 result: Ok(Action::TrackOfflineBounceCanceled { track_name }),
1015 } => assert_eq!(track_name, "track"),
1016 other => panic!("unexpected message: {other:?}"),
1017 }
1018 assert!(matches!(out_rx.recv().await, Some(Message::Ready(5))));
1019 let state_guard = state.lock();
1020 let track = state_guard.tracks.get("track").expect("track").lock();
1021 assert_eq!(track.level(), -9.0);
1022 assert_eq!(track.balance(), -0.3);
1023 }
1024
1025 #[cfg_attr(
1026 all(miri, target_os = "freebsd"),
1027 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
1028 )]
1029 #[tokio::test]
1030 async fn process_offline_bounce_restores_track_state_on_write_failure() {
1031 let (_rx_unused_tx, rx_unused) = channel(1);
1032 let (tx, mut out_rx) = channel(8);
1033 let worker = Worker {
1034 id: 3,
1035 rx: rx_unused,
1036 tx,
1037 realtime_priority: 0,
1038 };
1039 let track = Track::new("track".to_string(), 1, 2, 0, 0, 4, 48_000.0);
1040 track.set_level(-4.0);
1041 track.set_balance(0.25);
1042 let state = make_state_with_track(track);
1043 let output_path = std::env::temp_dir().to_string_lossy().to_string();
1044 let job = OfflineBounceWork {
1045 state: Arc::new(state.lock().snapshot()),
1046 track_name: "track".to_string(),
1047 output_path,
1048 start_sample: 0,
1049 length_samples: 4,
1050 tempo_bpm: 120.0,
1051 tsig_num: 4,
1052 tsig_denom: 4,
1053 automation_lanes: vec![],
1054 cancel: Arc::new(AtomicBool::new(false)),
1055 apply_fader: false,
1056 };
1057
1058 worker.process_offline_bounce(job).await;
1059
1060 let mut saw_error = false;
1061 while let Some(message) = out_rx.recv().await {
1062 match message {
1063 Message::OfflineBounceFinished {
1064 result: Ok(Action::TrackOfflineBounceProgress { .. }),
1065 } => {}
1066 Message::OfflineBounceFinished { result: Err(err) } => {
1067 assert!(
1068 err.contains("Failed to create offline bounce")
1069 || err.contains("Failed to write offline bounce")
1070 || err.contains("Failed to finalize offline bounce")
1071 );
1072 saw_error = true;
1073 }
1074 Message::Ready(3) => break,
1075 other => panic!("unexpected message: {other:?}"),
1076 }
1077 }
1078 assert!(saw_error);
1079 let state_guard = state.lock();
1080 let track = state_guard.tracks.get("track").expect("track").lock();
1081 assert_eq!(track.level(), -4.0);
1082 assert_eq!(track.balance(), 0.25);
1083 }
1084
1085 #[cfg_attr(
1086 all(miri, target_os = "freebsd"),
1087 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
1088 )]
1089 #[tokio::test]
1090 async fn process_offline_bounce_emits_progress_and_completion() {
1091 let (_rx_unused_tx, rx_unused) = channel(1);
1092 let (tx, mut out_rx) = channel(16);
1093 let worker = Worker {
1094 id: 2,
1095 rx: rx_unused,
1096 tx,
1097 realtime_priority: 0,
1098 };
1099 let track = Track::new("track".to_string(), 1, 1, 0, 0, 4, 48_000.0);
1100 track.set_level(-3.0);
1101 track.set_balance(0.4);
1102 let state = make_state_with_track(track);
1103 let output = unique_temp_wav("success");
1104 let job = OfflineBounceWork {
1105 state: Arc::new(state.lock().snapshot()),
1106 track_name: "track".to_string(),
1107 output_path: output.to_string_lossy().to_string(),
1108 start_sample: 0,
1109 length_samples: 8,
1110 tempo_bpm: 120.0,
1111 tsig_num: 4,
1112 tsig_denom: 4,
1113 automation_lanes: vec![],
1114 cancel: Arc::new(AtomicBool::new(false)),
1115 apply_fader: false,
1116 };
1117
1118 worker.process_offline_bounce(job).await;
1119
1120 let mut saw_progress = false;
1121 let mut saw_complete = false;
1122 let mut saw_ready = false;
1123 while let Some(message) = out_rx.recv().await {
1124 match message {
1125 Message::OfflineBounceFinished {
1126 result:
1127 Ok(Action::TrackOfflineBounceProgress {
1128 track_name,
1129 progress,
1130 ..
1131 }),
1132 } => {
1133 assert_eq!(track_name, "track");
1134 assert!(progress > 0.0);
1135 saw_progress = true;
1136 }
1137 Message::OfflineBounceFinished {
1138 result:
1139 Ok(Action::TrackOfflineBounce {
1140 track_name,
1141 output_path,
1142 ..
1143 }),
1144 } => {
1145 assert_eq!(track_name, "track");
1146 assert_eq!(output_path, output.to_string_lossy());
1147 saw_complete = true;
1148 }
1149 Message::Ready(2) => {
1150 saw_ready = true;
1151 break;
1152 }
1153 other => panic!("unexpected message: {other:?}"),
1154 }
1155 }
1156
1157 assert!(saw_progress);
1158 assert!(saw_complete);
1159 assert!(saw_ready);
1160 assert!(output.exists());
1161 std::fs::remove_file(&output).expect("remove temp wav");
1162 let state_guard = state.lock();
1163 let track = state_guard.tracks.get("track").expect("track").lock();
1164 assert_eq!(track.level(), -3.0);
1165 assert_eq!(track.balance(), 0.4);
1166 assert!(!track.muted());
1167 }
1168}