old school music tracker

rework audio worker comms

+105 -115
+102 -85
src/app.rs
··· 10 10 use smol::{channel::Sender, lock::Mutex}; 11 11 use torque_tracker_engine::{ 12 12 audio_processing::playback::PlaybackStatus, 13 - manager::{AudioManager, OutputConfig, PlaybackSettings, SendResult, ToWorkerMsg}, 13 + manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg}, 14 14 project::song::{Song, SongOperation}, 15 15 }; 16 16 use triple_buffer::triple_buffer; ··· 23 23 }; 24 24 25 25 use cpal::{ 26 - BufferSize, SupportedBufferSize, 26 + BufferSize, OutputStreamTimestamp, SupportedBufferSize, 27 27 traits::{DeviceTrait, HostTrait}, 28 28 }; 29 29 ··· 45 45 }; 46 46 47 47 pub static EXECUTOR: smol::Executor = smol::Executor::new(); 48 - pub static AUDIO: LazyLock<Mutex<AudioManager>> = 48 + /// Song data 49 + /// 50 + /// Be careful about locking order with AUDIO_OUTPUT_COMMS to not deadlock 51 + pub static SONG_MANAGER: LazyLock<smol::lock::Mutex<AudioManager>> = 49 52 LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); 53 + /// Sender for Song changes 50 54 pub static SONG_OP_SEND: OnceLock<smol::channel::Sender<SongOperation>> = OnceLock::new(); 51 55 52 56 /// shorter function name ··· 60 64 Header(HeaderEvent), 61 65 /// also closes all dialogs 62 66 GoToPage(PagesEnum), 67 + // Needed because only in the main app i know which pattern is selected, so i know what to play 63 68 Playback(PlaybackType), 64 69 CloseRequested, 65 70 CloseApp, ··· 162 167 header: Header, 163 168 event_loop_proxy: EventLoopProxy<GlobalEvent>, 164 169 worker_threads: Option<WorkerThreads>, 170 + // needed here because it isn't send. This Option should be synchronized with AUDIO_OUTPUT_COMMS 165 171 audio_stream: Option<( 166 172 cpal::Stream, 167 - triple_buffer::Output<Option<cpal::OutputStreamTimestamp>>, 168 173 smol::Task<()>, 174 + torque_tracker_engine::manager::StreamSend, 169 175 )>, 170 176 } 171 177 172 178 impl ApplicationHandler<GlobalEvent> for App { 173 179 fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { 174 180 if start_cause == winit::event::StartCause::Init { 175 - LazyLock::force(&AUDIO); 181 + LazyLock::force(&SONG_MANAGER); 176 182 self.worker_threads = Some(WorkerThreads::new()); 177 183 let (send, recv) = smol::channel::unbounded(); 178 184 SONG_OP_SEND.get_or_init(|| send); 179 185 EXECUTOR 180 186 .spawn(async move { 181 187 while let Ok(op) = recv.recv().await { 182 - let mut manager = AUDIO.lock().await; 183 - 184 - loop { 185 - if let Some(mut song) = manager.try_edit_song() { 186 - song.apply_operation(op).unwrap(); 187 - // don't need to relock if there are more operations in queue 188 - while let Ok(op) = recv.try_recv() { 189 - song.apply_operation(op).unwrap(); 190 - } 191 - break; 188 + let mut manager = SONG_MANAGER.lock().await; 189 + // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong 190 + let buffer_time = manager.last_buffer_time(); 191 + // spin loop to lock the song 192 + let mut song = loop { 193 + if let Some(song) = manager.try_edit_song() { 194 + break song; 192 195 } 193 - let buffer_time = manager.buffer_time().expect("locking failed once, so audio must be active, so there must be a buffer_time"); 196 + // smol mutex lock is held across await point 194 197 smol::Timer::after(buffer_time).await; 198 + }; 199 + // apply the received op 200 + song.apply_operation(op).unwrap(); 201 + // try to get more ops. This avoids repeated locking of the song when a lot of operations are 202 + // in queue 203 + while let Ok(op) = recv.try_recv() { 204 + song.apply_operation(op).unwrap(); 195 205 } 206 + drop(song); 196 207 } 197 208 }) 198 209 .detach(); ··· 200 211 EXECUTOR 201 212 .spawn(async { 202 213 loop { 203 - let mut lock = AUDIO.lock().await; 214 + let mut lock = SONG_MANAGER.lock().await; 204 215 lock.collect_garbage(); 205 216 drop(lock); 206 217 smol::Timer::after(Duration::from_secs(10)).await; ··· 280 291 return; 281 292 } 282 293 283 - if event.logical_key == Key::Named(NamedKey::F5) { 284 - self.event_queue 285 - .push_back(GlobalEvent::Playback(PlaybackType::Song)); 286 - } else if event.logical_key == Key::Named(NamedKey::F6) { 287 - if modifiers.state().shift_key() { 294 + if event.state.is_pressed() { 295 + if event.logical_key == Key::Named(NamedKey::F5) { 288 296 self.event_queue 289 - .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); 290 - } else { 297 + .push_back(GlobalEvent::Playback(PlaybackType::Song)); 298 + return; 299 + } else if event.logical_key == Key::Named(NamedKey::F6) { 300 + if modifiers.state().shift_key() { 301 + self.event_queue 302 + .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); 303 + } else { 304 + self.event_queue 305 + .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); 306 + } 307 + return; 308 + } else if event.logical_key == Key::Named(NamedKey::F8) { 291 309 self.event_queue 292 - .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); 310 + .push_back(GlobalEvent::Playback(PlaybackType::Stop)); 311 + return; 312 + } 313 + } 314 + // key_event didn't start or stop the song, so process normally 315 + if let Some(dialog) = dialog_manager.active_dialog_mut() { 316 + match dialog.process_input(&event, modifiers, event_queue) { 317 + DialogResponse::Close => { 318 + dialog_manager.close_dialog(); 319 + // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably 320 + ui_pages.request_draw_const(); 321 + window.request_redraw(); 322 + } 323 + DialogResponse::RequestRedraw => window.request_redraw(), 324 + DialogResponse::None => (), 293 325 } 294 - // TODO: add F7 handling 295 - } else if event.logical_key == Key::Named(NamedKey::F8) { 296 - self.event_queue 297 - .push_back(GlobalEvent::Playback(PlaybackType::Stop)); 298 - // TODO: allow F5 to also switch to the playback page, once it exists 299 326 } else { 300 - // key_event didn't start or stop the song, so process normally 301 - if let Some(dialog) = dialog_manager.active_dialog_mut() { 302 - match dialog.process_input(&event, modifiers, event_queue) { 303 - DialogResponse::Close => { 304 - dialog_manager.close_dialog(); 305 - // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably 306 - ui_pages.request_draw_const(); 307 - window.request_redraw(); 308 - } 309 - DialogResponse::RequestRedraw => window.request_redraw(), 310 - DialogResponse::None => (), 311 - } 312 - } else { 313 - if event.state.is_pressed() 314 - && event.logical_key == Key::Named(NamedKey::Escape) 315 - { 316 - event_queue.push_back(GlobalEvent::OpenDialog(Box::new(|| { 317 - Box::new(PageMenu::main()) 318 - }))); 319 - } 327 + if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) 328 + { 329 + event_queue.push_back(GlobalEvent::OpenDialog(Box::new(|| { 330 + Box::new(PageMenu::main()) 331 + }))); 332 + } 320 333 321 - match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { 322 - PageResponse::RequestRedraw => window.request_redraw(), 323 - PageResponse::None => (), 324 - } 334 + match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { 335 + PageResponse::RequestRedraw => window.request_redraw(), 336 + PageResponse::None => (), 325 337 } 326 338 } 327 339 } ··· 381 393 }; 382 394 383 395 if let Some(msg) = msg { 384 - let result = AUDIO.lock_blocking().try_msg_worker(msg); 385 - 386 - match result { 387 - SendResult::Success => (), 388 - SendResult::BufferFull => { 389 - panic!("to worker buffer full, probably have to retry somehow") 390 - } 391 - SendResult::AudioInactive => panic!("audio should always be active"), 392 - } 396 + self.audio_stream 397 + .as_mut() 398 + .expect( 399 + "audio stream should always be active, should still handle this error", 400 + ) 401 + .2 402 + .try_msg_worker(msg) 403 + .expect("buffer full. either increase size or retry somehow") 393 404 } 394 405 } 395 406 } ··· 474 485 config.buffer_size = BufferSize::Fixed(buffer_size); 475 486 (config, buffer_size) 476 487 }; 477 - let mut guard = AUDIO.lock_blocking(); 478 - let mut worker = guard.get_callback::<f32>(OutputConfig { 479 - buffer_size, 480 - channel_count: NonZero::new(config.channels).unwrap(), 481 - sample_rate: NonZero::new(config.sample_rate.0).unwrap(), 482 - }); 483 - let buffer_time = guard.buffer_time().unwrap(); 488 + let mut guard = SONG_MANAGER.lock_blocking(); 489 + let (mut worker, buffer_time, status, stream_send) = 490 + guard.get_callback::<f32>(OutputConfig { 491 + buffer_size, 492 + channel_count: NonZero::new(config.channels).unwrap(), 493 + sample_rate: NonZero::new(config.sample_rate.0).unwrap(), 494 + }); 484 495 // keep the guard as short as possible to not block the async threads 485 496 drop(guard); 486 - let (mut send, recv) = triple_buffer(&None); 497 + let (mut timestamp_send, recv) = triple_buffer(&None); 487 498 let stream = device 488 499 .build_output_stream( 489 500 &config, 490 501 move |data, info| { 491 502 worker(data); 492 - send.write(Some(info.timestamp())); 503 + timestamp_send.write(Some(info.timestamp())); 493 504 }, 494 505 |err| eprintln!("audio stream err: {err:?}"), 495 506 None, ··· 498 509 // spawn a task to process the audio playback status updates 499 510 let proxy = self.event_loop_proxy.clone(); 500 511 let task = EXECUTOR.spawn(async move { 501 - let mut buffer_time = buffer_time; 512 + let buffer_time = buffer_time; 513 + let mut status_recv = status; 514 + // maybe also send the timestamp every second or so 515 + let mut timestamp_recv = recv; 502 516 let mut old_status: Option<PlaybackStatus> = None; 517 + let mut old_timestamp: Option<OutputStreamTimestamp> = None; 503 518 loop { 504 - let mut lock = AUDIO.lock().await; 505 - let status = lock.playback_status().cloned(); 506 - let time = lock.buffer_time(); 507 - drop(lock); 508 - let status = status.expect("background task running while no stream active"); 519 + let status = *status_recv.get(); 509 520 // only react on status changes. could at some point be made more granular 510 521 if status != old_status { 511 522 old_status = status; ··· 528 539 ))) 529 540 .unwrap(); 530 541 } 531 - 532 - if let Some(time) = time { 533 - assert!(time == buffer_time); 534 - buffer_time = time; 542 + let timestamp = *timestamp_recv.read(); 543 + if timestamp != old_timestamp { 544 + // TODO: maybe send it somewhere 545 + old_timestamp = timestamp; 535 546 } 536 547 smol::Timer::after(buffer_time).await; 537 548 } 538 549 }); 539 - self.audio_stream = Some((stream, recv, task)); 550 + self.audio_stream = Some((stream, task, stream_send)); 540 551 } 541 552 542 553 fn close_audio_stream(&mut self) { 543 - _ = self.audio_stream.take().unwrap(); 544 - AUDIO.lock_blocking().stream_closed(); 554 + let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); 555 + // stop playback 556 + _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); 557 + _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); 558 + // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways 559 + drop(task); 560 + // lastly kill the audio stream 561 + drop(stream); 545 562 } 546 563 } 547 564
+1 -28
src/ui/pages.rs
··· 11 11 use pattern::{PatternPage, PatternPageEvent}; 12 12 use sample_list::SampleList; 13 13 use song_directory_config_page::{SDCChange, SongDirectoryConfigPage}; 14 - use torque_tracker_engine::manager::{PlaybackSettings, SendResult, ToWorkerMsg}; 15 14 use winit::{ 16 15 event::{KeyEvent, Modifiers}, 17 16 event_loop::EventLoopProxy, ··· 19 18 }; 20 19 21 20 use crate::{ 22 - app::{AUDIO, GlobalEvent}, 21 + app::GlobalEvent, 23 22 coordinates::{CharPosition, CharRect, WINDOW_SIZE_CHARS}, 24 23 draw_buffer::DrawBuffer, 25 24 ui::pages::sample_list::SampleListEvent, ··· 280 279 } else if key_event.logical_key == Key::Named(NamedKey::F3) { 281 280 self.switch_page(PagesEnum::SampleList); 282 281 return PageResponse::RequestRedraw; 283 - } else if key_event.logical_key == Key::Named(NamedKey::F6) { 284 - let result = AUDIO.lock_blocking().try_msg_worker(ToWorkerMsg::Playback( 285 - PlaybackSettings::Order { 286 - idx: 0, 287 - should_loop: false, 288 - }, 289 - )); 290 - match result { 291 - SendResult::Success => (), 292 - SendResult::BufferFull => { 293 - panic!("to worker buffer full, probably have to retry somehow") 294 - } 295 - SendResult::AudioInactive => panic!("audio should always be active"), 296 - } 297 - } else if key_event.logical_key == Key::Named(NamedKey::F8) { 298 - let result = AUDIO 299 - .lock_blocking() 300 - .try_msg_worker(ToWorkerMsg::StopPlayback); 301 - match result { 302 - SendResult::Success => (), 303 - SendResult::BufferFull => { 304 - panic!("to worker buffer full, probably have to retry somehow") 305 - } 306 - SendResult::AudioInactive => panic!("audio should always be active"), 307 - } 308 282 } 309 283 } 310 - // TODO: F6 play from start, F7 play from current pos, F8 stop 311 284 312 285 self.get_page_mut() 313 286 .process_key_event(modifiers, key_event, events)
+2 -2
src/ui/pages/pattern.rs
··· 12 12 }; 13 13 14 14 use crate::{ 15 - app::{AUDIO, EXECUTOR, GlobalEvent, send_song_op}, 15 + app::{EXECUTOR, GlobalEvent, SONG_MANAGER, send_song_op}, 16 16 coordinates::{CharPosition, CharRect}, 17 17 ui::header::HeaderEvent, 18 18 }; ··· 234 234 let proxy = self.event_proxy.clone(); 235 235 EXECUTOR 236 236 .spawn(async move { 237 - let lock = AUDIO.lock().await; 237 + let lock = SONG_MANAGER.lock().await; 238 238 let pattern = lock.get_song().patterns[usize::from(idx)].clone(); 239 239 drop(lock); 240 240 proxy