Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
20 kB · 479 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480use 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<GlobalEvent>,}
impl SampleList { const SAMPLE_VIEW_COUNT: u8 = 34; pub fn new(event_proxy: winit::event_loop::EventLoopProxy<GlobalEvent>) -> 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::<AsciiString>(); 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<str> = 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<bool> = 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<T>( buf: &mut Vec<f32>, in_buf: &symphonia::core::audio::AudioBuffer<T>, stereo: &mut Option<bool>, ) where T: symphonia::core::sample::Sample, f32: symphonia::core::conv::FromSample<T>, { 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!() }}