···1010use smol::{channel::Sender, lock::Mutex};
1111use torque_tracker_engine::{
1212 audio_processing::playback::PlaybackStatus,
1313- manager::{AudioManager, OutputConfig, PlaybackSettings, SendResult, ToWorkerMsg},
1313+ manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg},
1414 project::song::{Song, SongOperation},
1515};
1616use triple_buffer::triple_buffer;
···2323};
24242525use cpal::{
2626- BufferSize, SupportedBufferSize,
2626+ BufferSize, OutputStreamTimestamp, SupportedBufferSize,
2727 traits::{DeviceTrait, HostTrait},
2828};
2929···4545};
46464747pub static EXECUTOR: smol::Executor = smol::Executor::new();
4848-pub static AUDIO: LazyLock<Mutex<AudioManager>> =
4848+/// Song data
4949+///
5050+/// Be careful about locking order with AUDIO_OUTPUT_COMMS to not deadlock
5151+pub static SONG_MANAGER: LazyLock<smol::lock::Mutex<AudioManager>> =
4952 LazyLock::new(|| Mutex::new(AudioManager::new(Song::default())));
5353+/// Sender for Song changes
5054pub static SONG_OP_SEND: OnceLock<smol::channel::Sender<SongOperation>> = OnceLock::new();
51555256/// shorter function name
···6064 Header(HeaderEvent),
6165 /// also closes all dialogs
6266 GoToPage(PagesEnum),
6767+ // Needed because only in the main app i know which pattern is selected, so i know what to play
6368 Playback(PlaybackType),
6469 CloseRequested,
6570 CloseApp,
···162167 header: Header,
163168 event_loop_proxy: EventLoopProxy<GlobalEvent>,
164169 worker_threads: Option<WorkerThreads>,
170170+ // needed here because it isn't send. This Option should be synchronized with AUDIO_OUTPUT_COMMS
165171 audio_stream: Option<(
166172 cpal::Stream,
167167- triple_buffer::Output<Option<cpal::OutputStreamTimestamp>>,
168173 smol::Task<()>,
174174+ torque_tracker_engine::manager::StreamSend,
169175 )>,
170176}
171177172178impl ApplicationHandler<GlobalEvent> for App {
173179 fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) {
174180 if start_cause == winit::event::StartCause::Init {
175175- LazyLock::force(&AUDIO);
181181+ LazyLock::force(&SONG_MANAGER);
176182 self.worker_threads = Some(WorkerThreads::new());
177183 let (send, recv) = smol::channel::unbounded();
178184 SONG_OP_SEND.get_or_init(|| send);
179185 EXECUTOR
180186 .spawn(async move {
181187 while let Ok(op) = recv.recv().await {
182182- let mut manager = AUDIO.lock().await;
183183-184184- loop {
185185- if let Some(mut song) = manager.try_edit_song() {
186186- song.apply_operation(op).unwrap();
187187- // don't need to relock if there are more operations in queue
188188- while let Ok(op) = recv.try_recv() {
189189- song.apply_operation(op).unwrap();
190190- }
191191- break;
188188+ let mut manager = SONG_MANAGER.lock().await;
189189+ // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong
190190+ let buffer_time = manager.last_buffer_time();
191191+ // spin loop to lock the song
192192+ let mut song = loop {
193193+ if let Some(song) = manager.try_edit_song() {
194194+ break song;
192195 }
193193- let buffer_time = manager.buffer_time().expect("locking failed once, so audio must be active, so there must be a buffer_time");
196196+ // smol mutex lock is held across await point
194197 smol::Timer::after(buffer_time).await;
198198+ };
199199+ // apply the received op
200200+ song.apply_operation(op).unwrap();
201201+ // try to get more ops. This avoids repeated locking of the song when a lot of operations are
202202+ // in queue
203203+ while let Ok(op) = recv.try_recv() {
204204+ song.apply_operation(op).unwrap();
195205 }
206206+ drop(song);
196207 }
197208 })
198209 .detach();
···200211 EXECUTOR
201212 .spawn(async {
202213 loop {
203203- let mut lock = AUDIO.lock().await;
214214+ let mut lock = SONG_MANAGER.lock().await;
204215 lock.collect_garbage();
205216 drop(lock);
206217 smol::Timer::after(Duration::from_secs(10)).await;
···280291 return;
281292 }
282293283283- if event.logical_key == Key::Named(NamedKey::F5) {
284284- self.event_queue
285285- .push_back(GlobalEvent::Playback(PlaybackType::Song));
286286- } else if event.logical_key == Key::Named(NamedKey::F6) {
287287- if modifiers.state().shift_key() {
294294+ if event.state.is_pressed() {
295295+ if event.logical_key == Key::Named(NamedKey::F5) {
288296 self.event_queue
289289- .push_back(GlobalEvent::Playback(PlaybackType::FromOrder));
290290- } else {
297297+ .push_back(GlobalEvent::Playback(PlaybackType::Song));
298298+ return;
299299+ } else if event.logical_key == Key::Named(NamedKey::F6) {
300300+ if modifiers.state().shift_key() {
301301+ self.event_queue
302302+ .push_back(GlobalEvent::Playback(PlaybackType::FromOrder));
303303+ } else {
304304+ self.event_queue
305305+ .push_back(GlobalEvent::Playback(PlaybackType::Pattern));
306306+ }
307307+ return;
308308+ } else if event.logical_key == Key::Named(NamedKey::F8) {
291309 self.event_queue
292292- .push_back(GlobalEvent::Playback(PlaybackType::Pattern));
310310+ .push_back(GlobalEvent::Playback(PlaybackType::Stop));
311311+ return;
312312+ }
313313+ }
314314+ // key_event didn't start or stop the song, so process normally
315315+ if let Some(dialog) = dialog_manager.active_dialog_mut() {
316316+ match dialog.process_input(&event, modifiers, event_queue) {
317317+ DialogResponse::Close => {
318318+ dialog_manager.close_dialog();
319319+ // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably
320320+ ui_pages.request_draw_const();
321321+ window.request_redraw();
322322+ }
323323+ DialogResponse::RequestRedraw => window.request_redraw(),
324324+ DialogResponse::None => (),
293325 }
294294- // TODO: add F7 handling
295295- } else if event.logical_key == Key::Named(NamedKey::F8) {
296296- self.event_queue
297297- .push_back(GlobalEvent::Playback(PlaybackType::Stop));
298298- // TODO: allow F5 to also switch to the playback page, once it exists
299326 } else {
300300- // key_event didn't start or stop the song, so process normally
301301- if let Some(dialog) = dialog_manager.active_dialog_mut() {
302302- match dialog.process_input(&event, modifiers, event_queue) {
303303- DialogResponse::Close => {
304304- dialog_manager.close_dialog();
305305- // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably
306306- ui_pages.request_draw_const();
307307- window.request_redraw();
308308- }
309309- DialogResponse::RequestRedraw => window.request_redraw(),
310310- DialogResponse::None => (),
311311- }
312312- } else {
313313- if event.state.is_pressed()
314314- && event.logical_key == Key::Named(NamedKey::Escape)
315315- {
316316- event_queue.push_back(GlobalEvent::OpenDialog(Box::new(|| {
317317- Box::new(PageMenu::main())
318318- })));
319319- }
327327+ if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape)
328328+ {
329329+ event_queue.push_back(GlobalEvent::OpenDialog(Box::new(|| {
330330+ Box::new(PageMenu::main())
331331+ })));
332332+ }
320333321321- match ui_pages.process_key_event(&self.modifiers, &event, event_queue) {
322322- PageResponse::RequestRedraw => window.request_redraw(),
323323- PageResponse::None => (),
324324- }
334334+ match ui_pages.process_key_event(&self.modifiers, &event, event_queue) {
335335+ PageResponse::RequestRedraw => window.request_redraw(),
336336+ PageResponse::None => (),
325337 }
326338 }
327339 }
···381393 };
382394383395 if let Some(msg) = msg {
384384- let result = AUDIO.lock_blocking().try_msg_worker(msg);
385385-386386- match result {
387387- SendResult::Success => (),
388388- SendResult::BufferFull => {
389389- panic!("to worker buffer full, probably have to retry somehow")
390390- }
391391- SendResult::AudioInactive => panic!("audio should always be active"),
392392- }
396396+ self.audio_stream
397397+ .as_mut()
398398+ .expect(
399399+ "audio stream should always be active, should still handle this error",
400400+ )
401401+ .2
402402+ .try_msg_worker(msg)
403403+ .expect("buffer full. either increase size or retry somehow")
393404 }
394405 }
395406 }
···474485 config.buffer_size = BufferSize::Fixed(buffer_size);
475486 (config, buffer_size)
476487 };
477477- let mut guard = AUDIO.lock_blocking();
478478- let mut worker = guard.get_callback::<f32>(OutputConfig {
479479- buffer_size,
480480- channel_count: NonZero::new(config.channels).unwrap(),
481481- sample_rate: NonZero::new(config.sample_rate.0).unwrap(),
482482- });
483483- let buffer_time = guard.buffer_time().unwrap();
488488+ let mut guard = SONG_MANAGER.lock_blocking();
489489+ let (mut worker, buffer_time, status, stream_send) =
490490+ guard.get_callback::<f32>(OutputConfig {
491491+ buffer_size,
492492+ channel_count: NonZero::new(config.channels).unwrap(),
493493+ sample_rate: NonZero::new(config.sample_rate.0).unwrap(),
494494+ });
484495 // keep the guard as short as possible to not block the async threads
485496 drop(guard);
486486- let (mut send, recv) = triple_buffer(&None);
497497+ let (mut timestamp_send, recv) = triple_buffer(&None);
487498 let stream = device
488499 .build_output_stream(
489500 &config,
490501 move |data, info| {
491502 worker(data);
492492- send.write(Some(info.timestamp()));
503503+ timestamp_send.write(Some(info.timestamp()));
493504 },
494505 |err| eprintln!("audio stream err: {err:?}"),
495506 None,
···498509 // spawn a task to process the audio playback status updates
499510 let proxy = self.event_loop_proxy.clone();
500511 let task = EXECUTOR.spawn(async move {
501501- let mut buffer_time = buffer_time;
512512+ let buffer_time = buffer_time;
513513+ let mut status_recv = status;
514514+ // maybe also send the timestamp every second or so
515515+ let mut timestamp_recv = recv;
502516 let mut old_status: Option<PlaybackStatus> = None;
517517+ let mut old_timestamp: Option<OutputStreamTimestamp> = None;
503518 loop {
504504- let mut lock = AUDIO.lock().await;
505505- let status = lock.playback_status().cloned();
506506- let time = lock.buffer_time();
507507- drop(lock);
508508- let status = status.expect("background task running while no stream active");
519519+ let status = *status_recv.get();
509520 // only react on status changes. could at some point be made more granular
510521 if status != old_status {
511522 old_status = status;
···528539 )))
529540 .unwrap();
530541 }
531531-532532- if let Some(time) = time {
533533- assert!(time == buffer_time);
534534- buffer_time = time;
542542+ let timestamp = *timestamp_recv.read();
543543+ if timestamp != old_timestamp {
544544+ // TODO: maybe send it somewhere
545545+ old_timestamp = timestamp;
535546 }
536547 smol::Timer::after(buffer_time).await;
537548 }
538549 });
539539- self.audio_stream = Some((stream, recv, task));
550550+ self.audio_stream = Some((stream, task, stream_send));
540551 }
541552542553 fn close_audio_stream(&mut self) {
543543- _ = self.audio_stream.take().unwrap();
544544- AUDIO.lock_blocking().stream_closed();
554554+ let (stream, task, mut stream_send) = self.audio_stream.take().unwrap();
555555+ // stop playback
556556+ _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback);
557557+ _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote);
558558+ // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways
559559+ drop(task);
560560+ // lastly kill the audio stream
561561+ drop(stream);
545562 }
546563}
547564
+1-28
src/ui/pages.rs
···1111use pattern::{PatternPage, PatternPageEvent};
1212use sample_list::SampleList;
1313use song_directory_config_page::{SDCChange, SongDirectoryConfigPage};
1414-use torque_tracker_engine::manager::{PlaybackSettings, SendResult, ToWorkerMsg};
1514use winit::{
1615 event::{KeyEvent, Modifiers},
1716 event_loop::EventLoopProxy,
···1918};
20192120use crate::{
2222- app::{AUDIO, GlobalEvent},
2121+ app::GlobalEvent,
2322 coordinates::{CharPosition, CharRect, WINDOW_SIZE_CHARS},
2423 draw_buffer::DrawBuffer,
2524 ui::pages::sample_list::SampleListEvent,
···280279 } else if key_event.logical_key == Key::Named(NamedKey::F3) {
281280 self.switch_page(PagesEnum::SampleList);
282281 return PageResponse::RequestRedraw;
283283- } else if key_event.logical_key == Key::Named(NamedKey::F6) {
284284- let result = AUDIO.lock_blocking().try_msg_worker(ToWorkerMsg::Playback(
285285- PlaybackSettings::Order {
286286- idx: 0,
287287- should_loop: false,
288288- },
289289- ));
290290- match result {
291291- SendResult::Success => (),
292292- SendResult::BufferFull => {
293293- panic!("to worker buffer full, probably have to retry somehow")
294294- }
295295- SendResult::AudioInactive => panic!("audio should always be active"),
296296- }
297297- } else if key_event.logical_key == Key::Named(NamedKey::F8) {
298298- let result = AUDIO
299299- .lock_blocking()
300300- .try_msg_worker(ToWorkerMsg::StopPlayback);
301301- match result {
302302- SendResult::Success => (),
303303- SendResult::BufferFull => {
304304- panic!("to worker buffer full, probably have to retry somehow")
305305- }
306306- SendResult::AudioInactive => panic!("audio should always be active"),
307307- }
308282 }
309283 }
310310- // TODO: F6 play from start, F7 play from current pos, F8 stop
311284312285 self.get_page_mut()
313286 .process_key_event(modifiers, key_event, events)