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