old school music tracker
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}