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 match task {
482 ProcessTask::Track(_) => {
483 let audio_out_count = t.audio.outs.len();
484 let metronome_output = Self::metronome_output_buffer(&plan, &t, outs);
485 let input_ptrs = ins
486 .iter()
487 .map(|&buf| {
488 unsafe { plan.buffer_ptr(buf) }
491 })
492 .collect::<Vec<_>>();
493 let mut inputs = input_ptrs
494 .iter()
495 .map(|&ptr| {
496 unsafe { (&mut *ptr).as_mut_slice() }
499 })
500 .collect::<Vec<_>>();
501 let source_buffers = Self::arena_source_slices(&plan, outs);
502 let output_ptrs = outs
503 .iter()
504 .take(audio_out_count)
505 .map(|&buf| {
506 unsafe { plan.buffer_ptr(buf) }
509 })
510 .collect::<Vec<_>>();
511 let mut outputs = output_ptrs
512 .iter()
513 .map(|&ptr| {
514 unsafe { (&mut *ptr).as_mut_slice() }
517 })
518 .collect::<Vec<_>>();
519 let metronome_output_ptr = metronome_output.map(|buf| {
520 unsafe { plan.buffer_ptr(buf) }
523 });
524 let metronome_output = metronome_output_ptr.map(|ptr| {
525 unsafe { (&mut *ptr).as_mut_slice() }
528 });
529 t.process_render_block_with_audio_buffers_and_metronome(
530 &mut inputs,
531 &mut outputs,
532 &source_buffers,
533 metronome_output,
534 );
535 for &out in outs.iter().take(audio_out_count) {
536 plan.set_buffer_latency(out, t.plugin_graph_latency_samples());
537 }
538 }
539 ProcessTask::FolderInput(_) => {
540 let metronome_output = Self::metronome_output_buffer(&plan, &t, outs);
541 let input_ptrs = ins
542 .iter()
543 .map(|&buf| {
544 unsafe { plan.buffer_ptr(buf) }
547 })
548 .collect::<Vec<_>>();
549 let mut inputs = input_ptrs
550 .iter()
551 .map(|&ptr| {
552 unsafe { (&mut *ptr).as_mut_slice() }
555 })
556 .collect::<Vec<_>>();
557 let metronome_output_ptr = metronome_output.map(|buf| {
558 unsafe { plan.buffer_ptr(buf) }
561 });
562 let metronome_output = metronome_output_ptr.map(|ptr| {
563 unsafe { (&mut *ptr).as_mut_slice() }
566 });
567 t.process_folder_input_with_audio_buffers_and_metronome(
568 &mut inputs,
569 metronome_output,
570 );
571 for &input in ins {
572 plan.set_buffer_latency(input, 0);
573 }
574 }
575 ProcessTask::FolderOutput(_) => {
576 let source_buffers = Self::arena_source_slices(&plan, outs);
577 let output_ptrs = outs
578 .iter()
579 .map(|&buf| {
580 unsafe { plan.buffer_ptr(buf) }
583 })
584 .collect::<Vec<_>>();
585 let mut outputs = output_ptrs
586 .iter()
587 .map(|&ptr| {
588 unsafe { (&mut *ptr).as_mut_slice() }
591 })
592 .collect::<Vec<_>>();
593 t.process_folder_output_with_audio_buffers(&mut outputs, &source_buffers);
594 for &out in outs {
595 plan.set_buffer_latency(out, t.plugin_graph_latency_samples());
596 }
597 }
598 ProcessTask::Plugin { kind, index, .. } => {
599 let input_latency = ins
600 .iter()
601 .map(|&input| plan.buffer_latency(input))
602 .max()
603 .unwrap_or(0);
604 let inputs = Self::arena_input_slices(&plan, ins);
605 let output_ptrs = outs
606 .iter()
607 .map(|&buf| {
608 unsafe { plan.buffer_ptr(buf) }
611 })
612 .collect::<Vec<_>>();
613 let mut outputs = output_ptrs
614 .iter()
615 .map(|&ptr| {
616 unsafe { (&mut *ptr).as_mut_slice() }
619 })
620 .collect::<Vec<_>>();
621 t.process_plugin_with_audio_buffers(*kind, *index, &inputs, &mut outputs);
622 let latency =
623 input_latency.saturating_add(t.plugin_latency_samples(*kind, *index));
624 for &out in outs {
625 plan.set_buffer_latency(out, latency);
626 }
627 }
628 }
629 t.audio.set_processing(false);
630 let latency_changed = t.take_plugin_latency_changed();
631 let updates = std::mem::take(&mut t.rt.echoed_parameter_updates);
632 let meter = t.output_meter_linear();
633 {
634 static TRACK_OUTPUT_PEAK_LOG_COUNT: std::sync::atomic::AtomicUsize =
635 std::sync::atomic::AtomicUsize::new(0);
636 let peak = meter.iter().copied().fold(0.0_f32, f32::max);
637 let count = TRACK_OUTPUT_PEAK_LOG_COUNT
638 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
639 if count < 64 || peak > 0.0 {
640 let task_kind = match task {
641 ProcessTask::Track(_) => "track",
642 ProcessTask::FolderInput(_) => "folder_input",
643 ProcessTask::FolderOutput(_) => "folder_output",
644 ProcessTask::Plugin { .. } => "plugin",
645 };
646 tracing::debug!(
647 worker_id,
648 node,
649 track = %t.name,
650 task_kind,
651 peak,
652 meter = ?meter,
653 "track output meter peak"
654 );
655 }
656 }
657 (meter, updates, latency_changed)
658 }
659 };
660 NodeJobResult {
661 worker_id,
662 epoch,
663 node,
664 output_linear,
665 parameter_updates,
666 latency_changed,
667 }
668 }
669
670 async fn process_node_job(&self, job: NodeJob) {
671 let result = Self::process_node_job_result(self.id, job);
672 let _ = self.tx.send(result.into()).await;
673 }
674
675 pub async fn work(&mut self) {
676 crate::enable_flush_denormals_to_zero();
677 if let Err(e) = Self::try_enable_realtime(self.realtime_priority) {
678 tracing::warn!(
679 "Worker {} realtime priority {} not enabled: {}",
680 self.id,
681 self.realtime_priority,
682 e
683 );
684 }
685 while let Some(message) = self.rx.recv().await {
686 match message {
687 Message::Request(Action::Quit) => {
688 return;
689 }
690 Message::ProcessOfflineBounce(job) => {
691 self.process_offline_bounce(job).await;
692 }
693 Message::NodeJob(job) => {
694 self.process_node_job(job).await;
695 }
696 _ => {}
697 }
698 }
699 }
700}
701
702impl From<NodeJobResult> for Message {
703 fn from(result: NodeJobResult) -> Self {
704 Message::NodeDone {
705 worker_id: result.worker_id,
706 epoch: result.epoch,
707 node: result.node,
708 output_linear: result.output_linear,
709 parameter_updates: result.parameter_updates,
710 latency_changed: result.latency_changed,
711 }
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::Worker;
718 use crate::message::{
719 Action, Message, OfflineAutomationLane, OfflineAutomationPoint, OfflineAutomationTarget,
720 OfflineBounceWork,
721 };
722 use crate::state::State;
723 use crate::track::Track;
724 use std::path::PathBuf;
725 use std::sync::{Arc, atomic::AtomicBool};
726 use std::time::{SystemTime, UNIX_EPOCH};
727 use tokio::sync::mpsc::channel;
728
729 fn make_state_with_track(track: Track) -> State {
730 let mut state = State::default();
731 state.tracks.insert(track.name.clone(), Arc::new(track));
732 state
733 }
734
735 fn unique_temp_wav(name: &str) -> PathBuf {
736 let nanos = SystemTime::now()
737 .duration_since(UNIX_EPOCH)
738 .expect("clock")
739 .as_nanos();
740 std::env::temp_dir().join(format!("maolan_{name}_{nanos}.wav"))
741 }
742
743 #[test]
744 fn prepare_track_for_freeze_render_neutralizes_level_and_balance() {
745 let mut track = Track::new("track".to_string(), 1, 2, 0, 0, 64, 48_000.0);
746 track.set_level(-6.0);
747 track.set_balance(0.35);
748
749 let (level, balance) = Worker::prepare_track_for_freeze_render(&mut track);
750
751 assert_eq!(level, -6.0);
752 assert_eq!(balance, 0.35);
753 assert_eq!(track.level(), 0.0);
754 assert_eq!(track.balance(), 0.0);
755
756 Worker::restore_track_after_freeze_render(&mut track, level, balance);
757 assert_eq!(track.level(), -6.0);
758 assert_eq!(track.balance(), 0.35);
759 }
760
761 #[test]
762 fn freeze_automation_ignores_volume_and_balance_lanes() {
763 let mut track = Track::new("track".to_string(), 1, 2, 0, 1, 64, 48_000.0);
764 let lanes = vec![
765 OfflineAutomationLane {
766 target: OfflineAutomationTarget::Volume,
767 visible: true,
768 points: vec![OfflineAutomationPoint {
769 sample: 0,
770 value: 0.0,
771 }],
772 },
773 OfflineAutomationLane {
774 target: OfflineAutomationTarget::Balance,
775 visible: true,
776 points: vec![OfflineAutomationPoint {
777 sample: 0,
778 value: 1.0,
779 }],
780 },
781 OfflineAutomationLane {
782 target: OfflineAutomationTarget::MidiCc { channel: 0, cc: 7 },
783 visible: true,
784 points: vec![OfflineAutomationPoint {
785 sample: 0,
786 value: 1.0,
787 }],
788 },
789 ];
790
791 Worker::apply_freeze_automation_at_sample(&mut track, 0, &lanes);
792
793 assert_eq!(track.level(), 0.0);
794 assert_eq!(track.balance(), 0.0);
795 assert_eq!(track.rt.pending_automation_midi_events.len(), 1);
796 assert_eq!(
797 track.rt.pending_automation_midi_events[0].data,
798 vec![0xB0, 7, 127]
799 );
800 }
801
802 #[test]
803 fn automation_lane_value_at_interpolates_between_points() {
804 let lane = OfflineAutomationLane {
805 target: OfflineAutomationTarget::Volume,
806 visible: true,
807 points: vec![
808 OfflineAutomationPoint {
809 sample: 10,
810 value: 0.25,
811 },
812 OfflineAutomationPoint {
813 sample: 20,
814 value: 0.75,
815 },
816 ],
817 };
818 let value = lane.value_at(15).expect("value");
819
820 assert!((value - 0.5).abs() < 1.0e-6);
821 }
822
823 #[test]
824 fn freeze_automation_applies_interpolated_midi_cc_lane() {
825 let mut track = Track::new("track".to_string(), 1, 1, 0, 1, 64, 48_000.0);
826 let lanes = vec![OfflineAutomationLane {
827 target: OfflineAutomationTarget::MidiCc { channel: 0, cc: 7 },
828 visible: true,
829 points: vec![
830 OfflineAutomationPoint {
831 sample: 0,
832 value: 0.0,
833 },
834 OfflineAutomationPoint {
835 sample: 10,
836 value: 1.0,
837 },
838 ],
839 }];
840
841 Worker::apply_freeze_automation_at_sample(&mut track, 5, &lanes);
842 assert_eq!(track.rt.pending_automation_midi_events.len(), 1);
843 assert_eq!(track.rt.pending_automation_midi_events[0].data[2], 64);
844
845 track.rt.pending_automation_midi_events.clear();
846 Worker::apply_freeze_automation_at_sample(&mut track, 2, &lanes);
847 assert_eq!(track.rt.pending_automation_midi_events.len(), 1);
848 assert_eq!(track.rt.pending_automation_midi_events[0].data[2], 25);
849 }
850
851 #[cfg_attr(
852 all(miri, target_os = "freebsd"),
853 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
854 )]
855 #[tokio::test]
856 async fn process_node_job_sums_arena_buffers() {
857 use crate::render_plan::{Op, RenderPlan};
858 use std::cell::UnsafeCell;
859 use std::collections::HashMap;
860
861 let (_rx_unused_tx, rx_unused) = channel(1);
862 let (tx, mut out_rx) = channel(8);
863 let worker = Worker {
864 id: 4,
865 rx: rx_unused,
866 tx,
867 realtime_priority: 0,
868 };
869 let collector = basedrop::Collector::new();
870 let plan = RenderPlan {
871 buffer_size: 4,
872 buffers: (0..3).map(|_| UnsafeCell::new(vec![0.0; 4])).collect(),
873 buffer_latencies: (0..3)
874 .map(|_| std::sync::atomic::AtomicUsize::new(0))
875 .collect(),
876 nodes: vec![Op::Sum {
877 inputs: vec![0, 1],
878 delays: vec![
879 UnsafeCell::new(crate::render_plan::DelayLine::new()),
880 UnsafeCell::new(crate::render_plan::DelayLine::new()),
881 ],
882 output: 2,
883 }],
884 indegree: vec![0],
885 dependents: vec![vec![]],
886 sources: vec![0],
887 hw_in_map: vec![],
888 hw_out_map: vec![],
889 port_map: HashMap::new(),
890 midi_edges: vec![],
891 forced: vec![],
892 };
893 unsafe {
895 (&mut *plan.buffer_ptr(0)).copy_from_slice(&[0.25, 0.5, 0.75, 1.0]);
896 (&mut *plan.buffer_ptr(1)).copy_from_slice(&[0.75, 0.5, 0.25, f32::NAN]);
897 }
898 let shared = std::sync::Arc::new(basedrop::Owned::new(&collector.handle(), plan));
899
900 worker
901 .process_node_job(crate::executor::NodeJob {
902 epoch: 1,
903 plan: shared.clone(),
904 node: 0,
905 })
906 .await;
907
908 unsafe {
911 assert_eq!(
912 &*shared.buffer_ptr(2),
913 &vec![1.0, 1.0, 1.0, 1.0],
914 "sanitized sum in the arena"
915 );
916 }
917 match out_rx.recv().await.expect("message") {
918 Message::NodeDone {
919 worker_id,
920 epoch,
921 node,
922 output_linear,
923 ..
924 } => {
925 assert_eq!(worker_id, 4);
926 assert_eq!(epoch, 1);
927 assert_eq!(node, 0);
928 assert!(output_linear.is_empty());
929 }
930 other => panic!("unexpected message: {other:?}"),
931 }
932 }
933
934 #[cfg_attr(
935 all(miri, target_os = "freebsd"),
936 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
937 )]
938 #[tokio::test]
939 async fn process_offline_bounce_errors_when_track_is_missing() {
940 let (_rx_unused_tx, rx_unused) = channel(1);
941 let (tx, mut out_rx) = channel(8);
942 let worker = Worker {
943 id: 7,
944 rx: rx_unused,
945 tx,
946 realtime_priority: 0,
947 };
948 let job = OfflineBounceWork {
949 state: Arc::new(State::default().snapshot()),
950 track_name: "missing".to_string(),
951 output_path: unique_temp_wav("missing").to_string_lossy().to_string(),
952 start_sample: 0,
953 length_samples: 8,
954 tempo_bpm: 120.0,
955 tsig_num: 4,
956 tsig_denom: 4,
957 automation_lanes: vec![],
958 cancel: Arc::new(AtomicBool::new(false)),
959 apply_fader: false,
960 };
961
962 worker.process_offline_bounce(job).await;
963
964 match out_rx.recv().await.expect("message") {
965 Message::OfflineBounceFinished { result: Err(err) } => {
966 assert!(err.contains("Track not found: missing"));
967 }
968 other => panic!("unexpected message: {other:?}"),
969 }
970 }
971
972 #[cfg_attr(
973 all(miri, target_os = "freebsd"),
974 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
975 )]
976 #[tokio::test]
977 async fn process_offline_bounce_cancels_and_restores_track_state() {
978 let (_rx_unused_tx, rx_unused) = channel(1);
979 let (tx, mut out_rx) = channel(8);
980 let worker = Worker {
981 id: 5,
982 rx: rx_unused,
983 tx,
984 realtime_priority: 0,
985 };
986 let track = Track::new("track".to_string(), 1, 2, 0, 0, 4, 48_000.0);
987 track.set_level(-9.0);
988 track.set_balance(-0.3);
989 let state = make_state_with_track(track);
990 let job = OfflineBounceWork {
991 state: Arc::new(state.lock().snapshot()),
992 track_name: "track".to_string(),
993 output_path: unique_temp_wav("cancel").to_string_lossy().to_string(),
994 start_sample: 0,
995 length_samples: 8,
996 tempo_bpm: 120.0,
997 tsig_num: 4,
998 tsig_denom: 4,
999 automation_lanes: vec![],
1000 cancel: Arc::new(AtomicBool::new(true)),
1001 apply_fader: false,
1002 };
1003
1004 worker.process_offline_bounce(job).await;
1005
1006 match out_rx.recv().await.expect("message") {
1007 Message::OfflineBounceFinished {
1008 result: Ok(Action::TrackOfflineBounceCanceled { track_name }),
1009 } => assert_eq!(track_name, "track"),
1010 other => panic!("unexpected message: {other:?}"),
1011 }
1012 assert!(matches!(out_rx.recv().await, Some(Message::Ready(5))));
1013 let state_guard = state.lock();
1014 let track = state_guard.tracks.get("track").expect("track").lock();
1015 assert_eq!(track.level(), -9.0);
1016 assert_eq!(track.balance(), -0.3);
1017 }
1018
1019 #[cfg_attr(
1020 all(miri, target_os = "freebsd"),
1021 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
1022 )]
1023 #[tokio::test]
1024 async fn process_offline_bounce_restores_track_state_on_write_failure() {
1025 let (_rx_unused_tx, rx_unused) = channel(1);
1026 let (tx, mut out_rx) = channel(8);
1027 let worker = Worker {
1028 id: 3,
1029 rx: rx_unused,
1030 tx,
1031 realtime_priority: 0,
1032 };
1033 let track = Track::new("track".to_string(), 1, 2, 0, 0, 4, 48_000.0);
1034 track.set_level(-4.0);
1035 track.set_balance(0.25);
1036 let state = make_state_with_track(track);
1037 let output_path = std::env::temp_dir().to_string_lossy().to_string();
1038 let job = OfflineBounceWork {
1039 state: Arc::new(state.lock().snapshot()),
1040 track_name: "track".to_string(),
1041 output_path,
1042 start_sample: 0,
1043 length_samples: 4,
1044 tempo_bpm: 120.0,
1045 tsig_num: 4,
1046 tsig_denom: 4,
1047 automation_lanes: vec![],
1048 cancel: Arc::new(AtomicBool::new(false)),
1049 apply_fader: false,
1050 };
1051
1052 worker.process_offline_bounce(job).await;
1053
1054 let mut saw_error = false;
1055 while let Some(message) = out_rx.recv().await {
1056 match message {
1057 Message::OfflineBounceFinished {
1058 result: Ok(Action::TrackOfflineBounceProgress { .. }),
1059 } => {}
1060 Message::OfflineBounceFinished { result: Err(err) } => {
1061 assert!(
1062 err.contains("Failed to create offline bounce")
1063 || err.contains("Failed to write offline bounce")
1064 || err.contains("Failed to finalize offline bounce")
1065 );
1066 saw_error = true;
1067 }
1068 Message::Ready(3) => break,
1069 other => panic!("unexpected message: {other:?}"),
1070 }
1071 }
1072 assert!(saw_error);
1073 let state_guard = state.lock();
1074 let track = state_guard.tracks.get("track").expect("track").lock();
1075 assert_eq!(track.level(), -4.0);
1076 assert_eq!(track.balance(), 0.25);
1077 }
1078
1079 #[cfg_attr(
1080 all(miri, target_os = "freebsd"),
1081 ignore = "Tokio runtime uses kqueue, which Miri does not support on FreeBSD"
1082 )]
1083 #[tokio::test]
1084 async fn process_offline_bounce_emits_progress_and_completion() {
1085 let (_rx_unused_tx, rx_unused) = channel(1);
1086 let (tx, mut out_rx) = channel(16);
1087 let worker = Worker {
1088 id: 2,
1089 rx: rx_unused,
1090 tx,
1091 realtime_priority: 0,
1092 };
1093 let track = Track::new("track".to_string(), 1, 1, 0, 0, 4, 48_000.0);
1094 track.set_level(-3.0);
1095 track.set_balance(0.4);
1096 let state = make_state_with_track(track);
1097 let output = unique_temp_wav("success");
1098 let job = OfflineBounceWork {
1099 state: Arc::new(state.lock().snapshot()),
1100 track_name: "track".to_string(),
1101 output_path: output.to_string_lossy().to_string(),
1102 start_sample: 0,
1103 length_samples: 8,
1104 tempo_bpm: 120.0,
1105 tsig_num: 4,
1106 tsig_denom: 4,
1107 automation_lanes: vec![],
1108 cancel: Arc::new(AtomicBool::new(false)),
1109 apply_fader: false,
1110 };
1111
1112 worker.process_offline_bounce(job).await;
1113
1114 let mut saw_progress = false;
1115 let mut saw_complete = false;
1116 let mut saw_ready = false;
1117 while let Some(message) = out_rx.recv().await {
1118 match message {
1119 Message::OfflineBounceFinished {
1120 result:
1121 Ok(Action::TrackOfflineBounceProgress {
1122 track_name,
1123 progress,
1124 ..
1125 }),
1126 } => {
1127 assert_eq!(track_name, "track");
1128 assert!(progress > 0.0);
1129 saw_progress = true;
1130 }
1131 Message::OfflineBounceFinished {
1132 result:
1133 Ok(Action::TrackOfflineBounce {
1134 track_name,
1135 output_path,
1136 ..
1137 }),
1138 } => {
1139 assert_eq!(track_name, "track");
1140 assert_eq!(output_path, output.to_string_lossy());
1141 saw_complete = true;
1142 }
1143 Message::Ready(2) => {
1144 saw_ready = true;
1145 break;
1146 }
1147 other => panic!("unexpected message: {other:?}"),
1148 }
1149 }
1150
1151 assert!(saw_progress);
1152 assert!(saw_complete);
1153 assert!(saw_ready);
1154 assert!(output.exists());
1155 std::fs::remove_file(&output).expect("remove temp wav");
1156 let state_guard = state.lock();
1157 let track = state_guard.tracks.get("track").expect("track").lock();
1158 assert_eq!(track.level(), -3.0);
1159 assert_eq!(track.balance(), 0.4);
1160 assert!(!track.muted());
1161 }
1162}