use std::{ io::{self, Write}, iter::zip, num::NonZero, str::from_utf8, }; use ascii::{AsciiChar, AsciiString}; use font8x8::UnicodeFonts; use torque_tracker_engine::{ project::{ note_event::Note, song::{Song, SongOperation}, }, sample::{Sample, SampleMetaData}, }; use winit::keyboard::{Key, NamedKey}; use crate::{ EXECUTOR, EventQueue, GlobalEvent, SONG_OP_SEND, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, header::HeaderEvent, pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, widgets::{NextWidget, WidgetResponse, text_in}, }; #[derive(Debug, Clone)] pub enum SampleListEvent { SetSample(u8, String, SampleMetaData), SelectSample(u8), } #[derive(PartialEq, Eq, Copy, Clone)] enum Cursor { Name(u8), Play, } pub struct SampleList { selected_sample: u8, cursor: Cursor, sample_view: u8, samples: [Option<(AsciiString, SampleMetaData)>; Song::MAX_SAMPLES_INSTR], event_proxy: winit::event_loop::EventLoopProxy, } impl SampleList { const SAMPLE_VIEW_COUNT: u8 = 34; pub fn new(event_proxy: winit::event_loop::EventLoopProxy) -> Self { Self { selected_sample: 0, samples: [const { None }; Song::MAX_SAMPLES_INSTR], sample_view: 0, event_proxy, cursor: Cursor::Name(0), } } pub fn process_event( &mut self, event: SampleListEvent, events: &mut EventQueue<'_>, ) -> PageResponse { match event { // this event is from the pattern page, so i don't have to send it there SampleListEvent::SelectSample(s) => { self.select_sample(s); self.send_to_header(events); PageResponse::RequestRedraw } SampleListEvent::SetSample(idx, name, meta) => { let name = name .chars() .flat_map(|c| AsciiChar::from_ascii(c).ok()) .collect::(); self.samples[usize::from(idx)] = Some((name, meta)); if self.selected_sample == idx { self.send_to_header(events); } PageResponse::RequestRedraw } } } fn select_sample(&mut self, sample: u8) { self.selected_sample = sample; self.sample_view = if self.selected_sample < self.sample_view { self.selected_sample } else if self.selected_sample > self.sample_view + Self::SAMPLE_VIEW_COUNT { self.selected_sample - Self::SAMPLE_VIEW_COUNT } else { self.sample_view }; } fn send_to_header(&self, events: &mut EventQueue<'_>) { let name: Box = self.samples[usize::from(self.selected_sample)] .as_ref() .map(|(n, _)| Box::from(n.as_str())) .unwrap_or(Box::from("")); events.push(GlobalEvent::Header(HeaderEvent::SetSample( self.selected_sample, name, ))); } fn send_to_pattern(&self, events: &mut EventQueue<'_>) { events.push(GlobalEvent::Page(PageEvent::Pattern( PatternPageEvent::SetSampleInstr(self.selected_sample), ))); } // exists so that this montrosity doesn't sit in the middle of the keyboard input code fn load_audio_file(&mut self) { let dialog = rfd::AsyncFileDialog::new() // TODO: figure out which formats i support and sync it with the symphonia features // .add_filter("supported audio formats", &["wav"]) .pick_file(); let proxy = self.event_proxy.clone(); let idx = self.selected_sample; EXECUTOR .spawn(async move { let file = dialog.await; let Some(file) = file else { return; }; let file_name = file.file_name(); // HOW TO SYMPHONIA: https://github.com/pdeljanov/Symphonia/blob/master/symphonia/examples/basic-interleaved.rs // IO is not async as symphonia doesn't support async IO. // This is fine as i have two background threads and don't // do IO that often. let Ok(file) = std::fs::File::open(file.path()) else { eprintln!("error opening file"); return; }; let mss = symphonia::core::io::MediaSourceStream::new(Box::new(file), Default::default()); let probe = symphonia::default::get_probe(); let Ok(probed) = probe.format( // TODO: add file extension to the hint &symphonia::core::probe::Hint::new(), mss, &Default::default(), &Default::default(), ) else { eprintln!("format error"); return; }; let mut format = probed.format; let Some(track) = format .tracks() .iter() .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) else { eprintln!("no decodable track found"); return; }; let Ok(mut decoder) = symphonia::default::get_codecs().make(&track.codec_params, &Default::default()) else { eprintln!("no decoder found"); return; }; let track_id = track.id; let Some(sample_rate) = track.codec_params.sample_rate else { eprintln!("no sample rate"); return; }; let Some(sample_rate) = NonZero::new(sample_rate) else { eprintln!("sample rate = 0"); return; }; let mut buf = Vec::new(); // i don't know yet. after the first iteration of the loop this is set let mut stereo: Option = None; loop { let packet = format.next_packet(); let packet = match packet { Ok(p) => p, // this is used as a end of stream signal. don't ask me why Err(symphonia::core::errors::Error::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => { break; } Err(e) => { eprintln!("decoding error: {e:?}"); return; } }; if packet.track_id() != track_id { continue; } match decoder.decode(&packet) { Ok(audio_buf) => { fn append_to_buf( buf: &mut Vec, in_buf: &symphonia::core::audio::AudioBuffer, stereo: &mut Option, ) where T: symphonia::core::sample::Sample, f32: symphonia::core::conv::FromSample, { use symphonia::core::{ audio::{Channels, Signal}, conv::FromSample, }; if in_buf .spec() .channels .contains(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) { // stereo + plus maybe other channels that i ignore assert!(stereo.is_none() || *stereo == Some(true)); *stereo = Some(true); let left = in_buf.chan(0); let right = in_buf.chan(1); assert!(left.len() == right.len()); let iter = zip(left, right).flat_map(|(l, r)| { [f32::from_sample(*l), f32::from_sample(*r)] }); buf.extend(iter); } else if in_buf.spec().channels.contains(Channels::FRONT_LEFT) { // assert not assert!(stereo.is_none() || *stereo == Some(false)); *stereo = Some(false); buf.extend( in_buf .chan(0) .iter() .map(|sample| f32::from_sample(*sample)), ); } else { eprintln!("no usable channel in sample data") } } use symphonia::core::audio::AudioBufferRef; match audio_buf { AudioBufferRef::U8(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::U16(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::U24(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::U32(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::S8(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::S16(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::S24(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::S32(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::F32(d) => append_to_buf(&mut buf, &d, &mut stereo), AudioBufferRef::F64(d) => append_to_buf(&mut buf, &d, &mut stereo), } } Err(symphonia::core::errors::Error::DecodeError(_)) => (), Err(_) => break, } } // hopefully both of these compile to a memcopy... let sample = if stereo.unwrap() { Sample::new_stereo_interpolated(buf) } else { Sample::new_mono(buf) }; // TODO: get the real metadata / sane defaults / configurable let meta = SampleMetaData { default_volume: 32, global_volume: 32, default_pan: None, vibrato_speed: 0, vibrato_depth: 0, vibrato_rate: 0, vibrato_waveform: Default::default(), sample_rate, base_note: Note::new(64).unwrap(), }; // send to UI proxy .send_event(GlobalEvent::Page(PageEvent::SampleList( SampleListEvent::SetSample(idx, file_name, meta), ))) .unwrap(); drop(proxy); // send to playback let operation = SongOperation::SetSample(idx, meta, sample); SONG_OP_SEND.get().unwrap().send(operation).await.unwrap(); }) .detach(); } } impl Page for SampleList { fn draw(&mut self, draw_buffer: &mut DrawBuffer) { // samples + play buttons const SAMPLE_BASE_POS: CharPosition = CharPosition::new(2, 13); const PLAY_BASE_POS: CharPosition = CharPosition::new(31, 13); let mut buf = [0; 2]; for (i, n) in (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() { let i = u8::try_from(i).unwrap(); // number let mut curse: io::Cursor<&mut [u8]> = io::Cursor::new(&mut buf); write!(curse, "{:02}", n).unwrap(); let str = from_utf8(&buf).unwrap(); draw_buffer.draw_string(str, SAMPLE_BASE_POS + CharPosition::new(0, i), 0, 2); // name let name = self.samples[usize::from(n)].as_ref().map(|(n, _)| n); let selected = self.selected_sample == n; let background_color = if selected { 14 } else { 0 }; let name_pos = SAMPLE_BASE_POS + CharPosition::new(3, i); draw_buffer.draw_string_length( name.unwrap_or(&AsciiString::new()).as_str(), name_pos, 24, 6, background_color, ); // if selected draw the text cursor by replacing one char if selected && let Cursor::Name(text_cursor) = self.cursor && let Some(name) = name { let cursor_char_pos = name_pos + CharPosition::new(text_cursor, 0); if usize::from(text_cursor) < name.len() { draw_buffer.draw_char( font8x8::BASIC_FONTS .get(name[usize::from(text_cursor)].into()) .unwrap(), cursor_char_pos, 0, 3, ); } else { draw_buffer.draw_rect(3, cursor_char_pos.into()); } } // play button let (fg_color, bg_color) = match (selected, name.is_some(), self.cursor == Cursor::Play) { // row not selected (false, false, _) => (7, 0), (false, true, _) => (6, 0), // row selected, sample inactive (true, false, false) => (7, 14), (true, false, true) => (0, 6), // row selected, sample active (true, true, false) => (6, 14), (true, true, true) => (0, 3), }; draw_buffer.show_colors(); draw_buffer.draw_string( "Play", PLAY_BASE_POS + CharPosition::new(0, i), fg_color, bg_color, ); } } fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { draw_buffer.draw_rect(2, CharRect::PAGE_AREA); } fn process_key_event( &mut self, modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, events: &mut EventQueue<'_>, ) -> PageResponse { // TODO: remove this once buttons exist on this page if !key_event.state.is_pressed() { return PageResponse::None; } match self.cursor { Cursor::Name(text_cursor) => { if key_event.logical_key == Key::Named(NamedKey::Tab) { if modifiers.state().shift_key() { // TODO: shift aroung to one of the buttons } else { self.cursor = Cursor::Play; } return PageResponse::RequestRedraw; } if let Some(sample) = &mut self.samples[usize::from(self.selected_sample)] { let mut text_cursor = usize::from(text_cursor); // text_editing let resp = text_in::process_input( &mut sample.0, 24, &NextWidget::default(), &mut text_cursor, None, modifiers, key_event, ); let text_cursor = u8::try_from(text_cursor).expect( "process input has increased the cursor outside of the text bounds", ); match resp { // no next widget specified WidgetResponse::SwitchFocus(_) => unreachable!(), // need to update the header WidgetResponse::RequestRedraw(true) => { self.send_to_header(events); self.cursor = Cursor::Name(text_cursor); // data changed, so early return redraw return PageResponse::RequestRedraw; } WidgetResponse::RequestRedraw(false) => { // cursor movement, so early return self.cursor = Cursor::Name(text_cursor); // here the header doesn't have to be updated, because only the // cursor position changed return PageResponse::RequestRedraw; } WidgetResponse::None => (), } } } Cursor::Play => { if key_event.logical_key == Key::Named(NamedKey::Tab) { if modifiers.state().shift_key() { // set the text_cursor to the end, because i came from the right let name_len = self.samples[usize::from(self.selected_sample)] .as_ref() .map(|(s, _)| s.len()) .unwrap_or(0); self.cursor = Cursor::Name(name_len.try_into().unwrap()); return PageResponse::RequestRedraw; } else { // TODO: move to one of the sample controls return PageResponse::None; } } // trigger a oneshot playback of the selected sample } } // if this matches the cursor is in the sample list if matches!(self.cursor, Cursor::Play | Cursor::Name(_)) { if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && modifiers.state().is_empty() { if let Some(s) = self.selected_sample.checked_sub(1) { self.select_sample(s); self.send_to_header(events); self.send_to_pattern(events); return PageResponse::RequestRedraw; } } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) && modifiers.state().is_empty() { if self.selected_sample + 1 < 100 { self.select_sample(self.selected_sample + 1); self.send_to_header(events); self.send_to_pattern(events); return PageResponse::RequestRedraw; } } else if key_event.logical_key == Key::Named(NamedKey::Enter) && modifiers.state().is_empty() { self.load_audio_file(); } // TODO: add PageUp and PageDown } else { todo!("other UI elements that are per sample") } PageResponse::None } #[cfg(feature = "accesskit")] fn build_tree( &self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, ) -> crate::AccessResponse { todo!() } }