1use crate::message::{
2 Action, Message, OfflineAutomationLane, OfflineAutomationPoint, OfflineAutomationTarget,
3 OfflineBounceWork,
4};
5#[cfg(unix)]
6use nix::libc;
7use std::collections::HashSet;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::mpsc::{Receiver, Sender};
11
12#[derive(Debug)]
13pub struct Worker {
14 id: usize,
15 rx: Receiver<Message>,
16 tx: Sender<Message>,
17 realtime_priority: i32,
18}
19
20impl Worker {
21 fn automation_lane_value_at(points: &[OfflineAutomationPoint], sample: usize) -> Option<f32> {
22 if points.is_empty() {
23 return None;
24 }
25 if sample <= points[0].sample {
26 return Some(points[0].value.clamp(0.0, 1.0));
27 }
28 if sample >= points[points.len().saturating_sub(1)].sample {
29 return Some(points[points.len().saturating_sub(1)].value.clamp(0.0, 1.0));
30 }
31 for segment in points.windows(2) {
32 let left = &segment[0];
33 let right = &segment[1];
34 if sample < left.sample || sample > right.sample {
35 continue;
36 }
37 let span = right.sample.saturating_sub(left.sample).max(1) as f32;
38 let t = (sample.saturating_sub(left.sample) as f32 / span).clamp(0.0, 1.0);
39 return Some((left.value + (right.value - left.value) * t).clamp(0.0, 1.0));
40 }
41 None
42 }
43
44 fn apply_freeze_automation_at_sample(
45 track: &mut crate::track::Track,
46 sample: usize,
47 lanes: &[OfflineAutomationLane],
48 ) {
49 for lane in lanes {
50 if matches!(
51 lane.target,
52 OfflineAutomationTarget::Volume | OfflineAutomationTarget::Balance
53 ) {
54 continue;
55 }
56 let Some(value) = Self::automation_lane_value_at(&lane.points, sample) else {
57 continue;
58 };
59 match lane.target {
60 OfflineAutomationTarget::Volume | OfflineAutomationTarget::Balance => {}
61 OfflineAutomationTarget::Mute => {
62 track.set_muted(value >= 0.5);
63 }
64 #[cfg(all(unix, not(target_os = "macos")))]
65 OfflineAutomationTarget::Lv2Parameter {
66 instance_id,
67 index,
68 min,
69 max,
70 } => {
71 let lo = min.min(max);
72 let hi = max.max(min);
73 let param_value = (lo + value * (hi - lo)).clamp(lo, hi);
74 let _ = track.set_lv2_control_value(
75 instance_id,
76 index as usize,
77 param_value as f64,
78 );
79 }
80 OfflineAutomationTarget::Vst3Parameter {
81 instance_id,
82 param_id,
83 } => {
84 let _ = track.set_vst3_parameter(instance_id, param_id, value.clamp(0.0, 1.0));
85 }
86 OfflineAutomationTarget::ClapParameter {
87 instance_id,
88 param_id,
89 min,
90 max,
91 } => {
92 let lo = min.min(max);
93 let hi = max.max(min);
94 let param_value = (lo + value as f64 * (hi - lo)).clamp(lo, hi);
95 let _ = track.set_clap_parameter_at(instance_id, param_id, param_value, 0);
96 }
97 }
98 }
99 }
100
101 fn prepare_track_for_freeze_render(track: &mut crate::track::Track) -> (f32, f32) {
102 let original_level = track.level();
103 let original_balance = track.balance;
104 track.set_level(0.0);
105 track.set_balance(0.0);
106 (original_level, original_balance)
107 }
108
109 fn restore_track_after_freeze_render(
110 track: &mut crate::track::Track,
111 original_level: f32,
112 original_balance: f32,
113 ) {
114 track.set_level(original_level);
115 track.set_balance(original_balance);
116 }
117
118 async fn process_offline_bounce(&self, job: OfflineBounceWork) {
119 let track_handle = job.state.lock().tracks.get(&job.track_name).cloned();
120 let Some(target_track) = track_handle else {
121 let _ = self
122 .tx
123 .send(Message::OfflineBounceFinished {
124 result: Err(format!("Track not found: {}", job.track_name)),
125 })
126 .await;
127 return;
128 };
129 let (channels, block_size, sample_rate) = {
130 let t = target_track.lock();
131 let block_size = t
132 .audio
133 .outs
134 .first()
135 .map(|io| io.buffer.lock().len())
136 .or_else(|| t.audio.ins.first().map(|io| io.buffer.lock().len()))
137 .unwrap_or(0)
138 .max(1);
139 (
140 t.audio.outs.len().max(1),
141 block_size,
142 t.sample_rate.round().max(1.0) as i32,
143 )
144 };
145 let freeze_state = if job.apply_fader {
146 None
147 } else {
148 let t = target_track.lock();
149 Some(Self::prepare_track_for_freeze_render(t))
150 };
151
152 let all_tracks: Vec<_> = job.state.lock().tracks.values().cloned().collect();
153 let mut output_to_track: std::collections::HashMap<usize, String> =
154 std::collections::HashMap::new();
155 for handle in &all_tracks {
156 let t = handle.lock();
157 for out in &t.audio.outs {
158 output_to_track.insert(Arc::as_ptr(out) as usize, t.name.clone());
159 }
160 }
161 let mut relevant_names = HashSet::new();
162 let mut queue = vec![job.track_name.clone()];
163 while let Some(name) = queue.pop() {
164 if !relevant_names.insert(name.clone()) {
165 continue;
166 }
167 if let Some(handle) = all_tracks.iter().find(|h| h.lock().name == name) {
168 let t = handle.lock();
169 for input in &t.audio.ins {
170 for conn in input.connections.lock().iter() {
171 if let Some(source_name) =
172 output_to_track.get(&(Arc::as_ptr(conn) as usize))
173 {
174 queue.push(source_name.clone());
175 }
176 }
177 }
178 }
179 }
180 let relevant_tracks: Vec<_> = all_tracks
181 .into_iter()
182 .filter(|h| relevant_names.contains(&h.lock().name))
183 .collect();
184
185 let mut output_samples =
186 Vec::<f32>::with_capacity(job.length_samples.saturating_mul(channels.max(1)));
187
188 let mut cursor = 0usize;
189 let mut last_reported_progress = 0.0_f32;
190 let mut total_process_time = Duration::ZERO;
191 let mut total_write_time = Duration::ZERO;
192 while cursor < job.length_samples {
193 if job.cancel.load(std::sync::atomic::Ordering::Relaxed) {
194 let _ = std::fs::remove_file(&job.output_path);
195 if let Some((original_level, original_balance)) = freeze_state {
196 let t = target_track.lock();
197 Self::restore_track_after_freeze_render(t, original_level, original_balance);
198 }
199 let _ = self
200 .tx
201 .send(Message::OfflineBounceFinished {
202 result: Ok(Action::TrackOfflineBounceCanceled {
203 track_name: job.track_name.clone(),
204 }),
205 })
206 .await;
207 let _ = self.tx.send(Message::Ready(self.id)).await;
208 return;
209 }
210
211 let step = (job.length_samples - cursor).min(block_size);
212 for handle in &relevant_tracks {
213 let t = handle.lock();
214 t.audio.finished = false;
215 t.audio.processing = false;
216 t.set_transport_sample(job.start_sample.saturating_add(cursor));
217 t.set_loop_config(false, None);
218 t.set_transport_timing(job.tempo_bpm, job.tsig_num, job.tsig_denom);
219 t.set_clip_playback_enabled(true);
220 t.set_record_tap_enabled(false);
221 }
222
223 loop {
224 let mut all_finished = true;
225 let mut progressed = false;
226 for handle in &relevant_tracks {
227 let t = handle.lock();
228 if t.audio.finished {
229 continue;
230 }
231 all_finished = false;
232 if !t.audio.processing && t.audio.ready() {
233 if t.name == job.track_name {
234 Self::apply_freeze_automation_at_sample(
235 t,
236 job.start_sample.saturating_add(cursor),
237 &job.automation_lanes,
238 );
239 }
240 t.audio.processing = true;
241 let p_start = Instant::now();
242 t.process();
243 total_process_time += p_start.elapsed();
244 t.audio.processing = false;
245 progressed = true;
246 }
247 }
248 if all_finished {
249 break;
250 }
251 if !progressed {
252 for handle in &relevant_tracks {
253 let t = handle.lock();
254 if t.audio.finished {
255 continue;
256 }
257 if t.name == job.track_name {
258 Self::apply_freeze_automation_at_sample(
259 t,
260 job.start_sample.saturating_add(cursor),
261 &job.automation_lanes,
262 );
263 }
264 t.audio.processing = true;
265 let p_start = Instant::now();
266 t.process();
267 total_process_time += p_start.elapsed();
268 t.audio.processing = false;
269 }
270 break;
271 }
272 }
273
274 let write_start = Instant::now();
275 {
276 let t = target_track.lock();
277 let outs: Vec<_> = (0..channels)
278 .map(|ch| t.audio.outs[ch].buffer.lock())
279 .collect();
280 for i in 0..step {
281 for out in outs.iter().take(channels) {
282 let sample = out.get(i).copied().unwrap_or(0.0);
283 output_samples.push(sample);
284 }
285 }
286 }
287 total_write_time += write_start.elapsed();
288
289 cursor = cursor.saturating_add(step);
290 let progress = (cursor as f32 / job.length_samples as f32).clamp(0.0, 1.0);
291
292 if progress - last_reported_progress >= 0.01 || cursor >= job.length_samples {
293 last_reported_progress = progress;
294 let _ = self
295 .tx
296 .send(Message::OfflineBounceFinished {
297 result: Ok(Action::TrackOfflineBounceProgress {
298 track_name: job.track_name.clone(),
299 progress,
300 operation: Some("Rendering freeze".to_string()),
301 }),
302 })
303 .await;
304 }
305 }
306
307 if let Err(e) = crate::audio_codec::write_wav_f32(
308 std::path::Path::new(&job.output_path),
309 &output_samples,
310 channels,
311 sample_rate as u32,
312 ) {
313 let _ = std::fs::remove_file(&job.output_path);
314 if let Some((original_level, original_balance)) = freeze_state {
315 let t = target_track.lock();
316 Self::restore_track_after_freeze_render(t, original_level, original_balance);
317 }
318 let _ = self
319 .tx
320 .send(Message::OfflineBounceFinished {
321 result: Err(format!(
322 "Failed to write offline bounce '{}': {e}",
323 job.output_path
324 )),
325 })
326 .await;
327 let _ = self.tx.send(Message::Ready(self.id)).await;
328 return;
329 }
330
331 if let Some((original_level, original_balance)) = freeze_state {
332 let t = target_track.lock();
333 Self::restore_track_after_freeze_render(t, original_level, original_balance);
334 }
335
336 let _ = self
337 .tx
338 .send(Message::OfflineBounceFinished {
339 result: Ok(Action::TrackOfflineBounce {
340 track_name: job.track_name,
341 output_path: job.output_path,
342 start_sample: job.start_sample,
343 length_samples: job.length_samples,
344 automation_lanes: vec![],
345 apply_fader: job.apply_fader,
346 }),
347 })
348 .await;
349 let _ = self.tx.send(Message::Ready(self.id)).await;
350 }
351
352 #[cfg(unix)]
353 fn try_enable_realtime(priority: i32) -> Result<(), String> {
354 let thread = unsafe { libc::pthread_self() };
355 let policy = libc::SCHED_FIFO;
356 let param = unsafe {
357 let mut p = std::mem::zeroed::<libc::sched_param>();
358 p.sched_priority = priority;
359 p
360 };
361 let rc = unsafe { libc::pthread_setschedparam(thread, policy, ¶m) };
362 if rc == 0 {
363 Ok(())
364 } else {
365 Err(format!("pthread_setschedparam failed with errno {}", rc))
366 }
367 }
368
369 #[cfg(not(unix))]
370 fn try_enable_realtime(_priority: i32) -> Result<(), String> {
371 Err("Realtime thread priority is not supported on this platform".to_string())
372 }
373
374 pub async fn new(
375 id: usize,
376 rx: Receiver<Message>,
377 tx: Sender<Message>,
378 realtime_priority: i32,
379 ) -> Worker {
380 let worker = Worker {
381 id,
382 rx,
383 tx,
384 realtime_priority,
385 };
386 worker.send(Message::Ready(id)).await;
387 worker
388 }
389
390 pub async fn send(&self, message: Message) {
391 self.tx
392 .send(message)
393 .await
394 .expect("Failed to send message from worker");
395 }
396
397 pub async fn work(&mut self) {
398 crate::enable_flush_denormals_to_zero();
399 let _ = Self::try_enable_realtime(self.realtime_priority);
400 while let Some(message) = self.rx.recv().await {
401 match message {
402 Message::Request(Action::Quit) => {
403 return;
404 }
405 Message::ProcessTrack(t) => {
406 let (track_name, output_linear, process_epoch, parameter_updates) = {
407 let track = t.lock();
408 let process_epoch = track.process_epoch;
409 track.process();
410 track.audio.processing = false;
411 let updates = std::mem::take(track.echoed_parameter_updates.lock());
412 (
413 track.name.clone(),
414 track.output_meter_linear(),
415 process_epoch,
416 updates,
417 )
418 };
419 let _ = self
420 .tx
421 .send(Message::Finished {
422 worker_id: self.id,
423 track_name,
424 output_linear,
425 process_epoch,
426 parameter_updates,
427 })
428 .await;
429 }
430 Message::ProcessOfflineBounce(job) => {
431 self.process_offline_bounce(job).await;
432 }
433 _ => {}
434 }
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::Worker;
442 use crate::message::{
443 Action, Message, OfflineAutomationLane, OfflineAutomationPoint, OfflineAutomationTarget,
444 OfflineBounceWork,
445 };
446 use crate::mutex::UnsafeMutex;
447 use crate::state::State;
448 use crate::track::Track;
449 use std::path::PathBuf;
450 use std::sync::{Arc, atomic::AtomicBool};
451 use std::time::{SystemTime, UNIX_EPOCH};
452 use tokio::sync::mpsc::channel;
453
454 fn make_state_with_track(track: Track) -> Arc<UnsafeMutex<State>> {
455 let mut state = State::default();
456 state.tracks.insert(
457 track.name.clone(),
458 Arc::new(UnsafeMutex::new(Box::new(track))),
459 );
460 Arc::new(UnsafeMutex::new(state))
461 }
462
463 fn unique_temp_wav(name: &str) -> PathBuf {
464 let nanos = SystemTime::now()
465 .duration_since(UNIX_EPOCH)
466 .expect("clock")
467 .as_nanos();
468 std::env::temp_dir().join(format!("maolan_{name}_{nanos}.wav"))
469 }
470
471 #[test]
472 fn prepare_track_for_freeze_render_neutralizes_level_and_balance() {
473 let mut track = Track::new("track".to_string(), 1, 2, 0, 0, 64, 48_000.0);
474 track.set_level(-6.0);
475 track.set_balance(0.35);
476
477 let (level, balance) = Worker::prepare_track_for_freeze_render(&mut track);
478
479 assert_eq!(level, -6.0);
480 assert_eq!(balance, 0.35);
481 assert_eq!(track.level(), 0.0);
482 assert_eq!(track.balance, 0.0);
483
484 Worker::restore_track_after_freeze_render(&mut track, level, balance);
485 assert_eq!(track.level(), -6.0);
486 assert_eq!(track.balance, 0.35);
487 }
488
489 #[test]
490 fn freeze_automation_ignores_volume_and_balance_lanes() {
491 let mut track = Track::new("track".to_string(), 1, 2, 0, 0, 64, 48_000.0);
492 let lanes = vec![
493 OfflineAutomationLane {
494 target: OfflineAutomationTarget::Volume,
495 points: vec![OfflineAutomationPoint {
496 sample: 0,
497 value: 0.0,
498 }],
499 },
500 OfflineAutomationLane {
501 target: OfflineAutomationTarget::Balance,
502 points: vec![OfflineAutomationPoint {
503 sample: 0,
504 value: 1.0,
505 }],
506 },
507 OfflineAutomationLane {
508 target: OfflineAutomationTarget::Mute,
509 points: vec![OfflineAutomationPoint {
510 sample: 0,
511 value: 1.0,
512 }],
513 },
514 ];
515
516 Worker::apply_freeze_automation_at_sample(&mut track, 0, &lanes);
517
518 assert_eq!(track.level(), 0.0);
519 assert_eq!(track.balance, 0.0);
520 assert!(track.muted);
521 }
522
523 #[test]
524 fn automation_lane_value_at_interpolates_between_points() {
525 let value = Worker::automation_lane_value_at(
526 &[
527 OfflineAutomationPoint {
528 sample: 10,
529 value: 0.25,
530 },
531 OfflineAutomationPoint {
532 sample: 20,
533 value: 0.75,
534 },
535 ],
536 15,
537 )
538 .expect("value");
539
540 assert!((value - 0.5).abs() < 1.0e-6);
541 }
542
543 #[test]
544 fn freeze_automation_applies_interpolated_mute_lane() {
545 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 64, 48_000.0);
546 let lanes = vec![OfflineAutomationLane {
547 target: OfflineAutomationTarget::Mute,
548 points: vec![
549 OfflineAutomationPoint {
550 sample: 0,
551 value: 0.0,
552 },
553 OfflineAutomationPoint {
554 sample: 10,
555 value: 1.0,
556 },
557 ],
558 }];
559
560 Worker::apply_freeze_automation_at_sample(&mut track, 5, &lanes);
561 assert!(track.muted);
562
563 track.set_muted(false);
564 Worker::apply_freeze_automation_at_sample(&mut track, 2, &lanes);
565 assert!(!track.muted);
566 }
567
568 #[tokio::test]
569 async fn process_offline_bounce_errors_when_track_is_missing() {
570 let (_rx_unused_tx, rx_unused) = channel(1);
571 let (tx, mut out_rx) = channel(8);
572 let worker = Worker {
573 id: 7,
574 rx: rx_unused,
575 tx,
576 realtime_priority: 0,
577 };
578 let job = OfflineBounceWork {
579 state: Arc::new(UnsafeMutex::new(State::default())),
580 track_name: "missing".to_string(),
581 output_path: unique_temp_wav("missing").to_string_lossy().to_string(),
582 start_sample: 0,
583 length_samples: 8,
584 tempo_bpm: 120.0,
585 tsig_num: 4,
586 tsig_denom: 4,
587 automation_lanes: vec![],
588 cancel: Arc::new(AtomicBool::new(false)),
589 apply_fader: false,
590 };
591
592 worker.process_offline_bounce(job).await;
593
594 match out_rx.recv().await.expect("message") {
595 Message::OfflineBounceFinished { result: Err(err) } => {
596 assert!(err.contains("Track not found: missing"));
597 }
598 other => panic!("unexpected message: {other:?}"),
599 }
600 }
601
602 #[tokio::test]
603 async fn process_offline_bounce_cancels_and_restores_track_state() {
604 let (_rx_unused_tx, rx_unused) = channel(1);
605 let (tx, mut out_rx) = channel(8);
606 let worker = Worker {
607 id: 5,
608 rx: rx_unused,
609 tx,
610 realtime_priority: 0,
611 };
612 let mut track = Track::new("track".to_string(), 1, 2, 0, 0, 4, 48_000.0);
613 track.set_level(-9.0);
614 track.set_balance(-0.3);
615 let state = make_state_with_track(track);
616 let job = OfflineBounceWork {
617 state: state.clone(),
618 track_name: "track".to_string(),
619 output_path: unique_temp_wav("cancel").to_string_lossy().to_string(),
620 start_sample: 0,
621 length_samples: 8,
622 tempo_bpm: 120.0,
623 tsig_num: 4,
624 tsig_denom: 4,
625 automation_lanes: vec![],
626 cancel: Arc::new(AtomicBool::new(true)),
627 apply_fader: false,
628 };
629
630 worker.process_offline_bounce(job).await;
631
632 match out_rx.recv().await.expect("message") {
633 Message::OfflineBounceFinished {
634 result: Ok(Action::TrackOfflineBounceCanceled { track_name }),
635 } => assert_eq!(track_name, "track"),
636 other => panic!("unexpected message: {other:?}"),
637 }
638 assert!(matches!(out_rx.recv().await, Some(Message::Ready(5))));
639 let track = state.lock().tracks.get("track").expect("track").lock();
640 assert_eq!(track.level(), -9.0);
641 assert_eq!(track.balance, -0.3);
642 }
643
644 #[tokio::test]
645 async fn process_offline_bounce_restores_track_state_on_write_failure() {
646 let (_rx_unused_tx, rx_unused) = channel(1);
647 let (tx, mut out_rx) = channel(8);
648 let worker = Worker {
649 id: 3,
650 rx: rx_unused,
651 tx,
652 realtime_priority: 0,
653 };
654 let mut track = Track::new("track".to_string(), 1, 2, 0, 0, 4, 48_000.0);
655 track.set_level(-4.0);
656 track.set_balance(0.25);
657 let state = make_state_with_track(track);
658 let output_path = std::env::temp_dir().to_string_lossy().to_string();
659 let job = OfflineBounceWork {
660 state: state.clone(),
661 track_name: "track".to_string(),
662 output_path,
663 start_sample: 0,
664 length_samples: 4,
665 tempo_bpm: 120.0,
666 tsig_num: 4,
667 tsig_denom: 4,
668 automation_lanes: vec![],
669 cancel: Arc::new(AtomicBool::new(false)),
670 apply_fader: false,
671 };
672
673 worker.process_offline_bounce(job).await;
674
675 let mut saw_error = false;
676 while let Some(message) = out_rx.recv().await {
677 match message {
678 Message::OfflineBounceFinished {
679 result: Ok(Action::TrackOfflineBounceProgress { .. }),
680 } => {}
681 Message::OfflineBounceFinished { result: Err(err) } => {
682 assert!(
683 err.contains("Failed to create offline bounce")
684 || err.contains("Failed to write offline bounce")
685 || err.contains("Failed to finalize offline bounce")
686 );
687 saw_error = true;
688 }
689 Message::Ready(3) => break,
690 other => panic!("unexpected message: {other:?}"),
691 }
692 }
693 assert!(saw_error);
694 let track = state.lock().tracks.get("track").expect("track").lock();
695 assert_eq!(track.level(), -4.0);
696 assert_eq!(track.balance, 0.25);
697 }
698
699 #[tokio::test]
700 async fn process_offline_bounce_emits_progress_and_completion() {
701 let (_rx_unused_tx, rx_unused) = channel(1);
702 let (tx, mut out_rx) = channel(16);
703 let worker = Worker {
704 id: 2,
705 rx: rx_unused,
706 tx,
707 realtime_priority: 0,
708 };
709 let mut track = Track::new("track".to_string(), 1, 1, 0, 0, 4, 48_000.0);
710 track.set_level(-3.0);
711 track.set_balance(0.4);
712 let state = make_state_with_track(track);
713 let output = unique_temp_wav("success");
714 let job = OfflineBounceWork {
715 state: state.clone(),
716 track_name: "track".to_string(),
717 output_path: output.to_string_lossy().to_string(),
718 start_sample: 0,
719 length_samples: 8,
720 tempo_bpm: 120.0,
721 tsig_num: 4,
722 tsig_denom: 4,
723 automation_lanes: vec![],
724 cancel: Arc::new(AtomicBool::new(false)),
725 apply_fader: false,
726 };
727
728 worker.process_offline_bounce(job).await;
729
730 let mut saw_progress = false;
731 let mut saw_complete = false;
732 let mut saw_ready = false;
733 while let Some(message) = out_rx.recv().await {
734 match message {
735 Message::OfflineBounceFinished {
736 result:
737 Ok(Action::TrackOfflineBounceProgress {
738 track_name,
739 progress,
740 ..
741 }),
742 } => {
743 assert_eq!(track_name, "track");
744 assert!(progress > 0.0);
745 saw_progress = true;
746 }
747 Message::OfflineBounceFinished {
748 result:
749 Ok(Action::TrackOfflineBounce {
750 track_name,
751 output_path,
752 ..
753 }),
754 } => {
755 assert_eq!(track_name, "track");
756 assert_eq!(output_path, output.to_string_lossy());
757 saw_complete = true;
758 }
759 Message::Ready(2) => {
760 saw_ready = true;
761 break;
762 }
763 other => panic!("unexpected message: {other:?}"),
764 }
765 }
766
767 assert!(saw_progress);
768 assert!(saw_complete);
769 assert!(saw_ready);
770 assert!(output.exists());
771 std::fs::remove_file(&output).expect("remove temp wav");
772 let track = state.lock().tracks.get("track").expect("track").lock();
773 assert_eq!(track.level(), -3.0);
774 assert_eq!(track.balance, 0.4);
775 assert!(!track.muted);
776 }
777}