old school music tracker
at dev 479 lines 20 kB view raw
1use std::{ 2 io::{self, Write}, 3 iter::zip, 4 num::NonZero, 5 str::from_utf8, 6}; 7 8use ascii::{AsciiChar, AsciiString}; 9use font8x8::UnicodeFonts; 10use torque_tracker_engine::{ 11 project::{ 12 note_event::Note, 13 song::{Song, SongOperation}, 14 }, 15 sample::{Sample, SampleMetaData}, 16}; 17use winit::keyboard::{Key, NamedKey}; 18 19use crate::{ 20 EXECUTOR, EventQueue, GlobalEvent, SONG_OP_SEND, 21 coordinates::{CharPosition, CharRect}, 22 draw_buffer::DrawBuffer, 23 header::HeaderEvent, 24 pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, 25 widgets::{NextWidget, WidgetResponse, text_in}, 26}; 27 28#[derive(Debug, Clone)] 29pub enum SampleListEvent { 30 SetSample(u8, String, SampleMetaData), 31 SelectSample(u8), 32} 33 34#[derive(PartialEq, Eq, Copy, Clone)] 35enum Cursor { 36 Name(u8), 37 Play, 38} 39 40pub struct SampleList { 41 selected_sample: u8, 42 cursor: Cursor, 43 sample_view: u8, 44 samples: [Option<(AsciiString, SampleMetaData)>; Song::MAX_SAMPLES_INSTR], 45 event_proxy: winit::event_loop::EventLoopProxy<GlobalEvent>, 46} 47 48impl SampleList { 49 const SAMPLE_VIEW_COUNT: u8 = 34; 50 pub fn new(event_proxy: winit::event_loop::EventLoopProxy<GlobalEvent>) -> Self { 51 Self { 52 selected_sample: 0, 53 samples: [const { None }; Song::MAX_SAMPLES_INSTR], 54 sample_view: 0, 55 event_proxy, 56 cursor: Cursor::Name(0), 57 } 58 } 59 60 pub fn process_event( 61 &mut self, 62 event: SampleListEvent, 63 events: &mut EventQueue<'_>, 64 ) -> PageResponse { 65 match event { 66 // this event is from the pattern page, so i don't have to send it there 67 SampleListEvent::SelectSample(s) => { 68 self.select_sample(s); 69 self.send_to_header(events); 70 PageResponse::RequestRedraw 71 } 72 SampleListEvent::SetSample(idx, name, meta) => { 73 let name = name 74 .chars() 75 .flat_map(|c| AsciiChar::from_ascii(c).ok()) 76 .collect::<AsciiString>(); 77 self.samples[usize::from(idx)] = Some((name, meta)); 78 if self.selected_sample == idx { 79 self.send_to_header(events); 80 } 81 PageResponse::RequestRedraw 82 } 83 } 84 } 85 86 fn select_sample(&mut self, sample: u8) { 87 self.selected_sample = sample; 88 self.sample_view = if self.selected_sample < self.sample_view { 89 self.selected_sample 90 } else if self.selected_sample > self.sample_view + Self::SAMPLE_VIEW_COUNT { 91 self.selected_sample - Self::SAMPLE_VIEW_COUNT 92 } else { 93 self.sample_view 94 }; 95 } 96 97 fn send_to_header(&self, events: &mut EventQueue<'_>) { 98 let name: Box<str> = self.samples[usize::from(self.selected_sample)] 99 .as_ref() 100 .map(|(n, _)| Box::from(n.as_str())) 101 .unwrap_or(Box::from("")); 102 events.push(GlobalEvent::Header(HeaderEvent::SetSample( 103 self.selected_sample, 104 name, 105 ))); 106 } 107 108 fn send_to_pattern(&self, events: &mut EventQueue<'_>) { 109 events.push(GlobalEvent::Page(PageEvent::Pattern( 110 PatternPageEvent::SetSampleInstr(self.selected_sample), 111 ))); 112 } 113 114 // exists so that this montrosity doesn't sit in the middle of the keyboard input code 115 fn load_audio_file(&mut self) { 116 let dialog = rfd::AsyncFileDialog::new() 117 // TODO: figure out which formats i support and sync it with the symphonia features 118 // .add_filter("supported audio formats", &["wav"]) 119 .pick_file(); 120 let proxy = self.event_proxy.clone(); 121 let idx = self.selected_sample; 122 EXECUTOR 123 .spawn(async move { 124 let file = dialog.await; 125 let Some(file) = file else { 126 return; 127 }; 128 let file_name = file.file_name(); 129 // HOW TO SYMPHONIA: https://github.com/pdeljanov/Symphonia/blob/master/symphonia/examples/basic-interleaved.rs 130 // IO is not async as symphonia doesn't support async IO. 131 // This is fine as i have two background threads and don't 132 // do IO that often. 133 let Ok(file) = std::fs::File::open(file.path()) else { 134 eprintln!("error opening file"); 135 return; 136 }; 137 let mss = 138 symphonia::core::io::MediaSourceStream::new(Box::new(file), Default::default()); 139 let probe = symphonia::default::get_probe(); 140 let Ok(probed) = probe.format( 141 // TODO: add file extension to the hint 142 &symphonia::core::probe::Hint::new(), 143 mss, 144 &Default::default(), 145 &Default::default(), 146 ) else { 147 eprintln!("format error"); 148 return; 149 }; 150 let mut format = probed.format; 151 let Some(track) = format 152 .tracks() 153 .iter() 154 .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) 155 else { 156 eprintln!("no decodable track found"); 157 return; 158 }; 159 let Ok(mut decoder) = 160 symphonia::default::get_codecs().make(&track.codec_params, &Default::default()) 161 else { 162 eprintln!("no decoder found"); 163 return; 164 }; 165 let track_id = track.id; 166 let Some(sample_rate) = track.codec_params.sample_rate else { 167 eprintln!("no sample rate"); 168 return; 169 }; 170 let Some(sample_rate) = NonZero::new(sample_rate) else { 171 eprintln!("sample rate = 0"); 172 return; 173 }; 174 let mut buf = Vec::new(); 175 // i don't know yet. after the first iteration of the loop this is set 176 let mut stereo: Option<bool> = None; 177 loop { 178 let packet = format.next_packet(); 179 let packet = match packet { 180 Ok(p) => p, 181 // this is used as a end of stream signal. don't ask me why 182 Err(symphonia::core::errors::Error::IoError(e)) 183 if e.kind() == std::io::ErrorKind::UnexpectedEof => 184 { 185 break; 186 } 187 Err(e) => { 188 eprintln!("decoding error: {e:?}"); 189 return; 190 } 191 }; 192 193 if packet.track_id() != track_id { 194 continue; 195 } 196 match decoder.decode(&packet) { 197 Ok(audio_buf) => { 198 fn append_to_buf<T>( 199 buf: &mut Vec<f32>, 200 in_buf: &symphonia::core::audio::AudioBuffer<T>, 201 stereo: &mut Option<bool>, 202 ) where 203 T: symphonia::core::sample::Sample, 204 f32: symphonia::core::conv::FromSample<T>, 205 { 206 use symphonia::core::{ 207 audio::{Channels, Signal}, 208 conv::FromSample, 209 }; 210 if in_buf 211 .spec() 212 .channels 213 .contains(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) 214 { 215 // stereo + plus maybe other channels that i ignore 216 assert!(stereo.is_none() || *stereo == Some(true)); 217 *stereo = Some(true); 218 let left = in_buf.chan(0); 219 let right = in_buf.chan(1); 220 assert!(left.len() == right.len()); 221 let iter = zip(left, right).flat_map(|(l, r)| { 222 [f32::from_sample(*l), f32::from_sample(*r)] 223 }); 224 buf.extend(iter); 225 } else if in_buf.spec().channels.contains(Channels::FRONT_LEFT) { 226 // assert not 227 assert!(stereo.is_none() || *stereo == Some(false)); 228 *stereo = Some(false); 229 buf.extend( 230 in_buf 231 .chan(0) 232 .iter() 233 .map(|sample| f32::from_sample(*sample)), 234 ); 235 } else { 236 eprintln!("no usable channel in sample data") 237 } 238 } 239 use symphonia::core::audio::AudioBufferRef; 240 match audio_buf { 241 AudioBufferRef::U8(d) => append_to_buf(&mut buf, &d, &mut stereo), 242 AudioBufferRef::U16(d) => append_to_buf(&mut buf, &d, &mut stereo), 243 AudioBufferRef::U24(d) => append_to_buf(&mut buf, &d, &mut stereo), 244 AudioBufferRef::U32(d) => append_to_buf(&mut buf, &d, &mut stereo), 245 AudioBufferRef::S8(d) => append_to_buf(&mut buf, &d, &mut stereo), 246 AudioBufferRef::S16(d) => append_to_buf(&mut buf, &d, &mut stereo), 247 AudioBufferRef::S24(d) => append_to_buf(&mut buf, &d, &mut stereo), 248 AudioBufferRef::S32(d) => append_to_buf(&mut buf, &d, &mut stereo), 249 AudioBufferRef::F32(d) => append_to_buf(&mut buf, &d, &mut stereo), 250 AudioBufferRef::F64(d) => append_to_buf(&mut buf, &d, &mut stereo), 251 } 252 } 253 Err(symphonia::core::errors::Error::DecodeError(_)) => (), 254 Err(_) => break, 255 } 256 } 257 // hopefully both of these compile to a memcopy... 258 let sample = if stereo.unwrap() { 259 Sample::new_stereo_interpolated(buf) 260 } else { 261 Sample::new_mono(buf) 262 }; 263 // TODO: get the real metadata / sane defaults / configurable 264 let meta = SampleMetaData { 265 default_volume: 32, 266 global_volume: 32, 267 default_pan: None, 268 vibrato_speed: 0, 269 vibrato_depth: 0, 270 vibrato_rate: 0, 271 vibrato_waveform: Default::default(), 272 sample_rate, 273 base_note: Note::new(64).unwrap(), 274 }; 275 // send to UI 276 proxy 277 .send_event(GlobalEvent::Page(PageEvent::SampleList( 278 SampleListEvent::SetSample(idx, file_name, meta), 279 ))) 280 .unwrap(); 281 drop(proxy); 282 // send to playback 283 let operation = SongOperation::SetSample(idx, meta, sample); 284 SONG_OP_SEND.get().unwrap().send(operation).await.unwrap(); 285 }) 286 .detach(); 287 } 288} 289 290impl Page for SampleList { 291 fn draw(&mut self, draw_buffer: &mut DrawBuffer) { 292 // samples + play buttons 293 const SAMPLE_BASE_POS: CharPosition = CharPosition::new(2, 13); 294 const PLAY_BASE_POS: CharPosition = CharPosition::new(31, 13); 295 let mut buf = [0; 2]; 296 for (i, n) in (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() { 297 let i = u8::try_from(i).unwrap(); 298 // number 299 let mut curse: io::Cursor<&mut [u8]> = io::Cursor::new(&mut buf); 300 write!(curse, "{:02}", n).unwrap(); 301 let str = from_utf8(&buf).unwrap(); 302 draw_buffer.draw_string(str, SAMPLE_BASE_POS + CharPosition::new(0, i), 0, 2); 303 304 // name 305 let name = self.samples[usize::from(n)].as_ref().map(|(n, _)| n); 306 let selected = self.selected_sample == n; 307 let background_color = if selected { 14 } else { 0 }; 308 let name_pos = SAMPLE_BASE_POS + CharPosition::new(3, i); 309 draw_buffer.draw_string_length( 310 name.unwrap_or(&AsciiString::new()).as_str(), 311 name_pos, 312 24, 313 6, 314 background_color, 315 ); 316 // if selected draw the text cursor by replacing one char 317 if selected 318 && let Cursor::Name(text_cursor) = self.cursor 319 && let Some(name) = name 320 { 321 let cursor_char_pos = name_pos + CharPosition::new(text_cursor, 0); 322 if usize::from(text_cursor) < name.len() { 323 draw_buffer.draw_char( 324 font8x8::BASIC_FONTS 325 .get(name[usize::from(text_cursor)].into()) 326 .unwrap(), 327 cursor_char_pos, 328 0, 329 3, 330 ); 331 } else { 332 draw_buffer.draw_rect(3, cursor_char_pos.into()); 333 } 334 } 335 336 // play button 337 let (fg_color, bg_color) = match (selected, name.is_some(), self.cursor == Cursor::Play) 338 { 339 // row not selected 340 (false, false, _) => (7, 0), 341 (false, true, _) => (6, 0), 342 // row selected, sample inactive 343 (true, false, false) => (7, 14), 344 (true, false, true) => (0, 6), 345 // row selected, sample active 346 (true, true, false) => (6, 14), 347 (true, true, true) => (0, 3), 348 }; 349 draw_buffer.show_colors(); 350 draw_buffer.draw_string( 351 "Play", 352 PLAY_BASE_POS + CharPosition::new(0, i), 353 fg_color, 354 bg_color, 355 ); 356 } 357 } 358 359 fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { 360 draw_buffer.draw_rect(2, CharRect::PAGE_AREA); 361 } 362 363 fn process_key_event( 364 &mut self, 365 modifiers: &winit::event::Modifiers, 366 key_event: &winit::event::KeyEvent, 367 events: &mut EventQueue<'_>, 368 ) -> PageResponse { 369 // TODO: remove this once buttons exist on this page 370 if !key_event.state.is_pressed() { 371 return PageResponse::None; 372 } 373 374 match self.cursor { 375 Cursor::Name(text_cursor) => { 376 if key_event.logical_key == Key::Named(NamedKey::Tab) { 377 if modifiers.state().shift_key() { 378 // TODO: shift aroung to one of the buttons 379 } else { 380 self.cursor = Cursor::Play; 381 } 382 return PageResponse::RequestRedraw; 383 } 384 if let Some(sample) = &mut self.samples[usize::from(self.selected_sample)] { 385 let mut text_cursor = usize::from(text_cursor); 386 // text_editing 387 let resp = text_in::process_input( 388 &mut sample.0, 389 24, 390 &NextWidget::default(), 391 &mut text_cursor, 392 None, 393 modifiers, 394 key_event, 395 ); 396 let text_cursor = u8::try_from(text_cursor).expect( 397 "process input has increased the cursor outside of the text bounds", 398 ); 399 match resp { 400 // no next widget specified 401 WidgetResponse::SwitchFocus(_) => unreachable!(), 402 // need to update the header 403 WidgetResponse::RequestRedraw(true) => { 404 self.send_to_header(events); 405 self.cursor = Cursor::Name(text_cursor); 406 // data changed, so early return redraw 407 return PageResponse::RequestRedraw; 408 } 409 WidgetResponse::RequestRedraw(false) => { 410 // cursor movement, so early return 411 self.cursor = Cursor::Name(text_cursor); 412 // here the header doesn't have to be updated, because only the 413 // cursor position changed 414 return PageResponse::RequestRedraw; 415 } 416 WidgetResponse::None => (), 417 } 418 } 419 } 420 Cursor::Play => { 421 if key_event.logical_key == Key::Named(NamedKey::Tab) { 422 if modifiers.state().shift_key() { 423 // set the text_cursor to the end, because i came from the right 424 let name_len = self.samples[usize::from(self.selected_sample)] 425 .as_ref() 426 .map(|(s, _)| s.len()) 427 .unwrap_or(0); 428 self.cursor = Cursor::Name(name_len.try_into().unwrap()); 429 return PageResponse::RequestRedraw; 430 } else { 431 // TODO: move to one of the sample controls 432 return PageResponse::None; 433 } 434 } 435 // trigger a oneshot playback of the selected sample 436 } 437 } 438 439 // if this matches the cursor is in the sample list 440 if matches!(self.cursor, Cursor::Play | Cursor::Name(_)) { 441 if key_event.logical_key == Key::Named(NamedKey::ArrowUp) 442 && modifiers.state().is_empty() 443 { 444 if let Some(s) = self.selected_sample.checked_sub(1) { 445 self.select_sample(s); 446 self.send_to_header(events); 447 self.send_to_pattern(events); 448 return PageResponse::RequestRedraw; 449 } 450 } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) 451 && modifiers.state().is_empty() 452 { 453 if self.selected_sample + 1 < 100 { 454 self.select_sample(self.selected_sample + 1); 455 self.send_to_header(events); 456 self.send_to_pattern(events); 457 return PageResponse::RequestRedraw; 458 } 459 } else if key_event.logical_key == Key::Named(NamedKey::Enter) 460 && modifiers.state().is_empty() 461 { 462 self.load_audio_file(); 463 } 464 // TODO: add PageUp and PageDown 465 } else { 466 todo!("other UI elements that are per sample") 467 } 468 469 PageResponse::None 470 } 471 472 #[cfg(feature = "accesskit")] 473 fn build_tree( 474 &self, 475 tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, 476 ) -> crate::AccessResponse { 477 todo!() 478 } 479}