old school music tracker
at main 595 lines 23 kB view raw
1use std::{ 2 collections::VecDeque, 3 fmt::Debug, 4 num::NonZero, 5 sync::{Arc, LazyLock, OnceLock}, 6 thread::JoinHandle, 7 time::Duration, 8}; 9 10use smol::{channel::Sender, lock::Mutex}; 11use torque_tracker_engine::{ 12 audio_processing::playback::PlaybackStatus, 13 manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg}, 14 project::song::{Song, SongOperation}, 15}; 16use triple_buffer::triple_buffer; 17use winit::{ 18 application::ApplicationHandler, 19 event::{Modifiers, WindowEvent}, 20 event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, 21 keyboard::{Key, NamedKey}, 22 window::{Window, WindowAttributes}, 23}; 24 25use cpal::{ 26 BufferSize, OutputStreamTimestamp, SupportedBufferSize, 27 traits::{DeviceTrait, HostTrait}, 28}; 29 30use crate::{ 31 palettes::Palette, 32 ui::pages::{order_list::OrderListPageEvent, pattern::PatternPageEvent}, 33}; 34 35use super::{ 36 draw_buffer::DrawBuffer, 37 render::RenderBackend, 38 ui::{ 39 dialog::{ 40 Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::PageMenu, 41 }, 42 header::{Header, HeaderEvent}, 43 pages::{AllPages, PageEvent, PageResponse, PagesEnum}, 44 }, 45}; 46 47pub static EXECUTOR: smol::Executor = smol::Executor::new(); 48/// Song data 49/// 50/// Be careful about locking order with AUDIO_OUTPUT_COMMS to not deadlock 51pub static SONG_MANAGER: LazyLock<smol::lock::Mutex<AudioManager>> = 52 LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); 53/// Sender for Song changes 54pub static SONG_OP_SEND: OnceLock<smol::channel::Sender<SongOperation>> = OnceLock::new(); 55 56/// shorter function name 57pub fn send_song_op(op: SongOperation) { 58 SONG_OP_SEND.get().unwrap().send_blocking(op).unwrap(); 59} 60 61pub enum GlobalEvent { 62 OpenDialog(Box<dyn FnOnce() -> Box<dyn Dialog> + Send>), 63 Page(PageEvent), 64 Header(HeaderEvent), 65 /// also closes all dialogs 66 GoToPage(PagesEnum), 67 // Needed because only in the main app i know which pattern is selected, so i know what to play 68 Playback(PlaybackType), 69 CloseRequested, 70 CloseApp, 71 ConstRedraw, 72} 73 74impl Clone for GlobalEvent { 75 fn clone(&self) -> Self { 76 // TODO: make this really clone, once the Dialogs are an enum instead of Box dyn 77 match self { 78 GlobalEvent::OpenDialog(_) => panic!("TODO: don't clone this"), 79 GlobalEvent::Page(page_event) => GlobalEvent::Page(page_event.clone()), 80 GlobalEvent::Header(header_event) => GlobalEvent::Header(header_event.clone()), 81 GlobalEvent::GoToPage(pages_enum) => GlobalEvent::GoToPage(*pages_enum), 82 GlobalEvent::CloseRequested => GlobalEvent::CloseRequested, 83 GlobalEvent::CloseApp => GlobalEvent::CloseApp, 84 GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, 85 GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), 86 } 87 } 88} 89 90impl Debug for GlobalEvent { 91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 92 let mut debug = f.debug_struct("GlobalEvent"); 93 match self { 94 GlobalEvent::OpenDialog(_) => debug.field("OpenDialog", &"closure"), 95 GlobalEvent::Page(page_event) => debug.field("Page", page_event), 96 GlobalEvent::Header(header_event) => debug.field("Header", header_event), 97 GlobalEvent::GoToPage(pages_enum) => debug.field("GoToPage", pages_enum), 98 GlobalEvent::CloseRequested => debug.field("CloseRequested", &""), 99 GlobalEvent::CloseApp => debug.field("CloseApp", &""), 100 GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), 101 GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), 102 }; 103 debug.finish() 104 } 105} 106 107#[derive(Clone, Copy, Debug)] 108pub enum PlaybackType { 109 Stop, 110 Song, 111 Pattern, 112 FromOrder, 113 FromCursor, 114} 115 116struct WorkerThreads { 117 handles: [JoinHandle<()>; 2], 118 close_msg: [Sender<()>; 2], 119} 120 121impl WorkerThreads { 122 fn new() -> Self { 123 let (send1, recv1) = smol::channel::unbounded(); 124 let thread1 = std::thread::Builder::new() 125 .name("Background Worker 1".into()) 126 .spawn(Self::worker_task(recv1)) 127 .unwrap(); 128 let (send2, recv2) = smol::channel::unbounded(); 129 let thread2 = std::thread::Builder::new() 130 .name("Background Worker 2".into()) 131 .spawn(Self::worker_task(recv2)) 132 .unwrap(); 133 134 Self { 135 handles: [thread1, thread2], 136 close_msg: [send1, send2], 137 } 138 } 139 140 fn worker_task(recv: smol::channel::Receiver<()>) -> impl FnOnce() + Send + 'static { 141 move || { 142 smol::block_on(EXECUTOR.run(async { recv.recv().await.unwrap() })); 143 } 144 } 145 146 /// prepares the closing of the threads by signalling them to stop 147 fn send_close(&mut self) { 148 _ = self.close_msg[0].send_blocking(()); 149 _ = self.close_msg[1].send_blocking(()); 150 } 151 152 fn close_all(mut self) { 153 self.send_close(); 154 let [handle1, handle2] = self.handles; 155 handle1.join().unwrap(); 156 handle2.join().unwrap(); 157 } 158} 159 160pub struct EventQueue<'a>(&'a mut VecDeque<GlobalEvent>); 161 162impl EventQueue<'_> { 163 pub fn push(&mut self, event: GlobalEvent) { 164 self.0.push_back(event); 165 } 166} 167 168pub struct App { 169 window_gpu: Option<(Arc<Window>, RenderBackend)>, 170 draw_buffer: DrawBuffer, 171 modifiers: Modifiers, 172 ui_pages: AllPages, 173 event_queue: VecDeque<GlobalEvent>, 174 dialog_manager: DialogManager, 175 header: Header, 176 event_loop_proxy: EventLoopProxy<GlobalEvent>, 177 worker_threads: Option<WorkerThreads>, 178 // needed here because it isn't send. This Option should be synchronized with AUDIO_OUTPUT_COMMS 179 audio_stream: Option<( 180 cpal::Stream, 181 smol::Task<()>, 182 torque_tracker_engine::manager::StreamSend, 183 )>, 184} 185 186impl ApplicationHandler<GlobalEvent> for App { 187 fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { 188 if start_cause == winit::event::StartCause::Init { 189 LazyLock::force(&SONG_MANAGER); 190 self.worker_threads = Some(WorkerThreads::new()); 191 let (send, recv) = smol::channel::unbounded(); 192 SONG_OP_SEND.get_or_init(|| send); 193 EXECUTOR 194 .spawn(async move { 195 while let Ok(op) = recv.recv().await { 196 let mut manager = SONG_MANAGER.lock().await; 197 // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong 198 let buffer_time = manager.last_buffer_time(); 199 // spin loop to lock the song 200 let mut song = loop { 201 if let Some(song) = manager.try_edit_song() { 202 break song; 203 } 204 // smol mutex lock is held across await point 205 smol::Timer::after(buffer_time).await; 206 }; 207 // apply the received op 208 song.apply_operation(op).unwrap(); 209 // try to get more ops. This avoids repeated locking of the song when a lot of operations are 210 // in queue 211 while let Ok(op) = recv.try_recv() { 212 song.apply_operation(op).unwrap(); 213 } 214 drop(song); 215 } 216 }) 217 .detach(); 218 // spawn a task to collect sample garbage every 10 seconds 219 EXECUTOR 220 .spawn(async { 221 loop { 222 let mut lock = SONG_MANAGER.lock().await; 223 lock.collect_garbage(); 224 drop(lock); 225 smol::Timer::after(Duration::from_secs(10)).await; 226 } 227 }) 228 .detach(); 229 self.start_audio_stream(); 230 } 231 } 232 233 fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { 234 self.build_window(event_loop); 235 } 236 237 fn suspended(&mut self, _: &ActiveEventLoop) { 238 // my window and GPU state have been invalidated 239 self.window_gpu = None; 240 } 241 242 fn window_event( 243 &mut self, 244 event_loop: &winit::event_loop::ActiveEventLoop, 245 _: winit::window::WindowId, 246 event: WindowEvent, 247 ) { 248 // destructure so i don't have to always type self. 249 let Self { 250 window_gpu, 251 draw_buffer, 252 modifiers, 253 ui_pages, 254 event_queue, 255 dialog_manager, 256 header, 257 event_loop_proxy: _, 258 worker_threads: _, 259 audio_stream: _, 260 } = self; 261 262 // panic is fine because when i get a window_event a window exists 263 let (window, render_backend) = window_gpu.as_mut().unwrap(); 264 // don't want the window to be mut 265 let window = window.as_ref(); 266 // limit the pages and widgets to only push events and not read or pop 267 let event_queue = &mut EventQueue(event_queue); 268 269 match event { 270 WindowEvent::CloseRequested => Self::close_requested(event_queue), 271 WindowEvent::Resized(physical_size) => { 272 render_backend.resize(physical_size); 273 window.request_redraw(); 274 } 275 WindowEvent::ScaleFactorChanged { 276 scale_factor: _, 277 inner_size_writer: _, 278 } => { 279 // window_state.resize(**new_inner_size); 280 // due to a version bump in winit i dont know anymore how to handle this event so i just ignore it for know and see if it makes problems in the future 281 // i have yet only received this event on linux wayland, not macos 282 println!("Window Scale Factor Changed"); 283 } 284 WindowEvent::RedrawRequested => { 285 // draw the new frame buffer 286 // TODO: split redraw header and redraw page. As soon as header gets a spectrometer this becomes important 287 header.draw(draw_buffer); 288 ui_pages.draw(draw_buffer); 289 dialog_manager.draw(draw_buffer); 290 // notify the windowing system that drawing is done and the new buffer is about to be pushed 291 window.pre_present_notify(); 292 // push the framebuffer into GPU/softbuffer and render it onto the screen 293 render_backend.render(&draw_buffer.framebuffer, event_loop); 294 } 295 WindowEvent::KeyboardInput { 296 device_id: _, 297 event, 298 is_synthetic, 299 } => { 300 if is_synthetic { 301 return; 302 } 303 304 if event.state.is_pressed() { 305 if event.logical_key == Key::Named(NamedKey::F5) { 306 self.event_queue 307 .push_back(GlobalEvent::Playback(PlaybackType::Song)); 308 return; 309 } else if event.logical_key == Key::Named(NamedKey::F6) { 310 if modifiers.state().shift_key() { 311 self.event_queue 312 .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); 313 } else { 314 self.event_queue 315 .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); 316 } 317 return; 318 } else if event.logical_key == Key::Named(NamedKey::F8) { 319 self.event_queue 320 .push_back(GlobalEvent::Playback(PlaybackType::Stop)); 321 return; 322 } 323 } 324 // key_event didn't start or stop the song, so process normally 325 if let Some(dialog) = dialog_manager.active_dialog_mut() { 326 match dialog.process_input(&event, modifiers, event_queue) { 327 DialogResponse::Close => { 328 dialog_manager.close_dialog(); 329 // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably 330 ui_pages.request_draw_const(); 331 window.request_redraw(); 332 } 333 DialogResponse::RequestRedraw => window.request_redraw(), 334 DialogResponse::None => (), 335 } 336 } else { 337 if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) 338 { 339 event_queue.push(GlobalEvent::OpenDialog(Box::new(|| { 340 Box::new(PageMenu::main()) 341 }))); 342 } 343 344 match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { 345 PageResponse::RequestRedraw => window.request_redraw(), 346 PageResponse::None => (), 347 } 348 } 349 } 350 // not sure if i need it just to make sure i always have all current modifiers to be used with keyboard events 351 WindowEvent::ModifiersChanged(new_modifiers) => *modifiers = new_modifiers, 352 353 _ => (), 354 } 355 356 while let Some(event) = self.event_queue.pop_front() { 357 self.user_event(event_loop, event); 358 } 359 } 360 361 /// i may need to add the ability for events to add events to the event queue, but that should be possible 362 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: GlobalEvent) { 363 let event_queue = &mut EventQueue(&mut self.event_queue); 364 match event { 365 GlobalEvent::OpenDialog(dialog) => { 366 self.dialog_manager.open_dialog(dialog()); 367 _ = self.try_request_redraw(); 368 } 369 GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { 370 PageResponse::RequestRedraw => _ = self.try_request_redraw(), 371 PageResponse::None => (), 372 }, 373 GlobalEvent::Header(header_event) => { 374 self.header.process_event(header_event); 375 _ = self.try_request_redraw(); 376 } 377 GlobalEvent::GoToPage(pages_enum) => { 378 self.dialog_manager.close_all(); 379 self.ui_pages.switch_page(pages_enum); 380 _ = self.try_request_redraw(); 381 } 382 GlobalEvent::CloseApp => event_loop.exit(), 383 GlobalEvent::CloseRequested => Self::close_requested(event_queue), 384 GlobalEvent::ConstRedraw => { 385 self.ui_pages.request_draw_const(); 386 _ = self.try_request_redraw(); 387 } 388 GlobalEvent::Playback(playback_type) => { 389 let msg = match playback_type { 390 PlaybackType::Song => Some(ToWorkerMsg::Playback(PlaybackSettings::Order { 391 idx: 0, 392 should_loop: true, 393 })), 394 PlaybackType::Stop => Some(ToWorkerMsg::StopPlayback), 395 PlaybackType::Pattern => { 396 Some(ToWorkerMsg::Playback(self.header.play_current_pattern())) 397 } 398 PlaybackType::FromOrder => { 399 Some(ToWorkerMsg::Playback(self.header.play_current_order())) 400 } 401 PlaybackType::FromCursor => None, 402 }; 403 404 if let Some(msg) = msg { 405 self.audio_stream 406 .as_mut() 407 .expect( 408 "audio stream should always be active, should still handle this error", 409 ) 410 .2 411 .try_msg_worker(msg) 412 .expect("buffer full. either increase size or retry somehow") 413 } 414 } 415 } 416 } 417 418 fn exiting(&mut self, _: &ActiveEventLoop) { 419 if let Some(workers) = self.worker_threads.take() { 420 // wait for all the threads to close 421 workers.close_all(); 422 } 423 if self.audio_stream.is_some() { 424 self.close_audio_stream(); 425 } 426 } 427} 428 429impl App { 430 pub fn new(proxy: EventLoopProxy<GlobalEvent>) -> Self { 431 Self { 432 window_gpu: None, 433 draw_buffer: DrawBuffer::new(), 434 modifiers: Modifiers::default(), 435 ui_pages: AllPages::new(proxy.clone()), 436 dialog_manager: DialogManager::new(), 437 header: Header::default(), 438 event_loop_proxy: proxy, 439 worker_threads: None, 440 audio_stream: None, 441 event_queue: VecDeque::with_capacity(3), 442 } 443 } 444 445 // TODO: should this be its own function?? or is there something better 446 fn close_requested(events: &mut EventQueue<'_>) { 447 events.push(GlobalEvent::OpenDialog(Box::new(|| { 448 Box::new(ConfirmDialog::new( 449 "Close Torque Tracker?", 450 || Some(GlobalEvent::CloseApp), 451 || None, 452 )) 453 }))); 454 } 455 456 /// tries to request a redraw. if there currently is no window this fails 457 fn try_request_redraw(&self) -> Result<(), ()> { 458 if let Some((window, _)) = &self.window_gpu { 459 window.request_redraw(); 460 Ok(()) 461 } else { 462 Err(()) 463 } 464 } 465 466 fn build_window(&mut self, event_loop: &ActiveEventLoop) { 467 self.window_gpu.get_or_insert_with(|| { 468 let mut attributes = WindowAttributes::default(); 469 attributes.active = true; 470 attributes.resizable = true; 471 attributes.resize_increments = None; 472 attributes.title = String::from("Torque Tracker"); 473 474 let window = Arc::new(event_loop.create_window(attributes).unwrap()); 475 let render_backend = RenderBackend::new(window.clone(), Palette::CAMOUFLAGE); 476 (window, render_backend) 477 }); 478 } 479 480 // TODO: make this configurable 481 fn start_audio_stream(&mut self) { 482 assert!(self.audio_stream.is_none()); 483 let host = cpal::default_host(); 484 let device = host.default_output_device().unwrap(); 485 let default_config = device.default_output_config().unwrap(); 486 let (config, buffer_size) = { 487 let mut config = default_config.config(); 488 let buffer_size = { 489 let default = default_config.buffer_size(); 490 match default { 491 SupportedBufferSize::Unknown => 1024, 492 SupportedBufferSize::Range { min, max } => u32::min(u32::max(1024, *min), *max), 493 } 494 }; 495 config.buffer_size = BufferSize::Fixed(buffer_size); 496 (config, buffer_size) 497 }; 498 let mut guard = SONG_MANAGER.lock_blocking(); 499 let (mut worker, buffer_time, status, stream_send) = 500 guard.get_callback::<f32>(OutputConfig { 501 buffer_size, 502 channel_count: NonZero::new(config.channels).unwrap(), 503 sample_rate: NonZero::new(config.sample_rate.0).unwrap(), 504 }); 505 // keep the guard as short as possible to not block the async threads 506 drop(guard); 507 let (mut timestamp_send, recv) = triple_buffer(&None); 508 let stream = device 509 .build_output_stream( 510 &config, 511 move |data, info| { 512 worker(data); 513 timestamp_send.write(Some(info.timestamp())); 514 }, 515 |err| eprintln!("audio stream err: {err:?}"), 516 None, 517 ) 518 .unwrap(); 519 // spawn a task to process the audio playback status updates 520 let proxy = self.event_loop_proxy.clone(); 521 let task = EXECUTOR.spawn(async move { 522 let buffer_time = buffer_time; 523 let mut status_recv = status; 524 // maybe also send the timestamp every second or so 525 let mut timestamp_recv = recv; 526 let mut old_status: Option<PlaybackStatus> = None; 527 let mut old_timestamp: Option<OutputStreamTimestamp> = None; 528 loop { 529 let status = *status_recv.get(); 530 // only react on status changes. could at some point be made more granular 531 if status != old_status { 532 old_status = status; 533 // println!("playback status: {status:?}"); 534 let pos = status.map(|s| s.position); 535 proxy 536 .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) 537 .unwrap(); 538 let pos = status.map(|s| (s.position.pattern, s.position.row)); 539 proxy 540 .send_event(GlobalEvent::Page(PageEvent::Pattern( 541 PatternPageEvent::PlaybackPosition(pos), 542 ))) 543 .unwrap(); 544 // does a map flatten. idk why it's called and_then 545 let pos = status.and_then(|s| s.position.order); 546 proxy 547 .send_event(GlobalEvent::Page(PageEvent::OrderList( 548 OrderListPageEvent::SetPlayback(pos), 549 ))) 550 .unwrap(); 551 } 552 let timestamp = *timestamp_recv.read(); 553 if timestamp != old_timestamp { 554 // TODO: maybe send it somewhere 555 old_timestamp = timestamp; 556 } 557 smol::Timer::after(buffer_time).await; 558 } 559 }); 560 self.audio_stream = Some((stream, task, stream_send)); 561 } 562 563 fn close_audio_stream(&mut self) { 564 let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); 565 // stop playback 566 _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); 567 _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); 568 // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways 569 drop(task); 570 // lastly kill the audio stream 571 drop(stream); 572 } 573} 574 575impl Drop for App { 576 fn drop(&mut self) { 577 if self.audio_stream.is_some() { 578 self.close_audio_stream(); 579 } 580 } 581} 582 583pub fn run() { 584 let event_loop = winit::event_loop::EventLoop::<GlobalEvent>::with_user_event() 585 .build() 586 .unwrap(); 587 event_loop.set_control_flow(ControlFlow::Wait); 588 // i don't need any raw device events. Keyboard and Mouse coming as window events are enough 589 event_loop.listen_device_events(winit::event_loop::DeviceEvents::Never); 590 let event_loop_proxy = event_loop.create_proxy(); 591 let mut app = App::new(event_loop_proxy); 592 app.header.draw_constant(&mut app.draw_buffer); 593 594 event_loop.run_app(&mut app).unwrap(); 595}