diff --git a/.gitignore b/.gitignore index 34beaa73..1a417612 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target perf.data* flamegraph*.svg vgcore* +example.mid diff --git a/crates/tek_api/examples/midi_import.rs b/crates/tek_api/examples/midi_import.rs new file mode 100644 index 00000000..b67d9e03 --- /dev/null +++ b/crates/tek_api/examples/midi_import.rs @@ -0,0 +1,18 @@ +use tek_api::*; + +struct ExamplePhrases(Vec>>); + +impl HasPhrases for ExamplePhrases { + fn phrases (&self) -> &Vec>> { + &self.0 + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.0 + } +} + +fn main () -> Usually<()> { + let mut phrases = ExamplePhrases(vec![]); + PhrasePoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; + Ok(()) +} diff --git a/crates/tek_api/src/api_phrase.rs b/crates/tek_api/src/api_phrase.rs index 8de378de..8d799dac 100644 --- a/crates/tek_api/src/api_phrase.rs +++ b/crates/tek_api/src/api_phrase.rs @@ -1,4 +1,5 @@ use crate::*; +use tek_core::midly::Smf; pub trait HasPhrases { fn phrases (&self) -> &Vec>>; @@ -10,11 +11,11 @@ pub enum PhrasePoolCommand { Add(usize, Phrase), Delete(usize), Swap(usize, usize), - Color(usize, ItemColor), Import(usize, String), Export(usize, String), SetName(usize, String), SetLength(usize, usize), + SetColor(usize, ItemColor), } impl Command for PhrasePoolCommand { @@ -39,12 +40,26 @@ impl Command for PhrasePoolCommand { model.phrases_mut().swap(index, other); return Ok(Some(Self::Swap(index, other))) }, - Self::Color(index, color) => { - let mut color = ItemColorTriplet::from(color); - std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); - return Ok(Some(Self::Color(index, color.base))) - }, Self::Import(index, path) => { + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + println!("{:?}", &smf.header); + let mut t = 0u32; + let mut events = vec![]; + for (i, track) in smf.tracks.iter().enumerate() { + for (j, event) in track.iter().enumerate() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); + } + } + } + let mut phrase = Phrase::new(&path, true, t as usize + 1, None, None); + for event in events.iter() { + println!("{event:?}"); + phrase.notes[event.0 as usize].push(event.2); + } + return Self::Add(index, phrase).execute(model) }, Self::Export(index, path) => { }, @@ -52,6 +67,11 @@ impl Command for PhrasePoolCommand { }, Self::SetLength(index, length) => { }, + Self::SetColor(index, color) => { + let mut color = ItemColorTriplet::from(color); + std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); + return Ok(Some(Self::SetColor(index, color.base))) + }, } Ok(None) } diff --git a/crates/tek_api/src/api_player.rs b/crates/tek_api/src/api_player.rs index 0fe5c549..1e6ba37f 100644 --- a/crates/tek_api/src/api_player.rs +++ b/crates/tek_api/src/api_player.rs @@ -11,9 +11,9 @@ pub trait HasPhrase: ClockApi { fn reset (&self) -> bool; fn reset_mut (&mut self) -> &mut bool; - fn phrase (&self) + fn play_phrase (&self) -> &Option<(Instant, Option>>)>; - fn phrase_mut (&mut self) + fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)>; fn next_phrase (&self) @@ -29,7 +29,7 @@ pub trait HasPhrase: ClockApi { *self.reset_mut() = true; } fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.phrase().as_ref() { + if let Some((started, Some(_))) = self.play_phrase().as_ref() { Some(self.current().pulse.get() - started.pulse.get()) } else { None @@ -71,7 +71,7 @@ pub trait MidiInputApi: ClockApi + HasPhrase { let sample0 = scope.last_frame_time() as usize; // For highlighting keys and note repeat let notes_in = self.notes_in().clone(); - if let (true, Some((started, ref phrase))) = (self.is_rolling(), self.phrase().clone()) { + if let (true, Some((started, ref phrase))) = (self.is_rolling(), self.play_phrase().clone()) { let start = started.sample.get() as usize; let quant = self.quant().get(); let timebase = self.timebase().clone(); @@ -166,8 +166,8 @@ pub trait MidiOutputApi: ClockApi + HasPhrase { let sample0 = scope.last_frame_time() as usize; let samples = scope.n_frames() as usize; // If no phrase is playing, prepare for switchover immediately - next = self.phrase().is_none(); - let phrase = self.phrase(); + next = self.play_phrase().is_none(); + let phrase = self.play_phrase(); let started0 = self.transport_offset(); let timebase = self.timebase(); let notes_out = self.notes_out(); @@ -241,7 +241,7 @@ pub trait MidiOutputApi: ClockApi + HasPhrase { let skipped = sample0 - start; // Switch over to enqueued phrase let started = Instant::from_sample(&self.timebase(), start as f64); - *self.phrase_mut() = Some((started, phrase.clone())); + *self.play_phrase_mut() = Some((started, phrase.clone())); // Unset enqueuement (TODO: where to implement looping?) *self.next_phrase_mut() = None } diff --git a/crates/tek_api/src/api_scene.rs b/crates/tek_api/src/api_scene.rs index 6459cc3d..c5356c1f 100644 --- a/crates/tek_api/src/api_scene.rs +++ b/crates/tek_api/src/api_scene.rs @@ -78,7 +78,7 @@ pub trait ArrangerSceneApi: Sized { Some(clip) => tracks .get(track_index) .map(|track|{ - if let Some((_, Some(phrase))) = track.phrase() { + if let Some((_, Some(phrase))) = track.play_phrase() { *phrase.read().unwrap() == *clip.read().unwrap() } else { false diff --git a/crates/tek_tui/src/tui_impls.rs b/crates/tek_tui/src/tui_impls.rs index 7b036eb1..3eaaec74 100644 --- a/crates/tek_tui/src/tui_impls.rs +++ b/crates/tek_tui/src/tui_impls.rs @@ -57,10 +57,10 @@ macro_rules! impl_midi_player { fn reset_mut (&mut self) -> &mut bool { &mut self$(.$field)*.reset } - fn phrase (&self) -> &Option<(Instant, Option>>)> { + fn play_phrase (&self) -> &Option<(Instant, Option>>)> { &self$(.$field)*.play_phrase } - fn phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { &mut self$(.$field)*.play_phrase } fn next_phrase (&self) -> &Option<(Instant, Option>>)> { diff --git a/crates/tek_tui/src/tui_input.rs b/crates/tek_tui/src/tui_input.rs index ecc56990..429f6fa0 100644 --- a/crates/tek_tui/src/tui_input.rs +++ b/crates/tek_tui/src/tui_input.rs @@ -97,18 +97,20 @@ fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { todo!() }, - AppFocus::Content(SequencerFocus::Transport(_)) => { - match TransportCommand::input_to_command(state, input)? { - TransportCommand::Clock(_) => { todo!() }, - TransportCommand::Focus(command) => Cmd::Focus(command), - } - }, - AppFocus::Content(SequencerFocus::Phrases) => { - Cmd::Phrases(PhrasesCommand::input_to_command(state, input)?) - }, - AppFocus::Content(SequencerFocus::PhraseEditor) => { - Cmd::Editor(PhraseCommand::input_to_command(state, input)?) - }, + AppFocus::Content(focused) => match focused { + SequencerFocus::Transport(_) => { + match TransportCommand::input_to_command(state, input)? { + TransportCommand::Clock(_) => { todo!() }, + TransportCommand::Focus(command) => Cmd::Focus(command), + } + }, + SequencerFocus::Phrases => { + Cmd::Phrases(PhrasesCommand::input_to_command(state, input)?) + }, + SequencerFocus::PhraseEditor => { + Cmd::Editor(PhraseCommand::input_to_command(state, input)?) + }, + } }) } @@ -118,43 +120,47 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option todo!(), - AppFocus::Content(ArrangerFocus::Transport(_)) => { - use TransportCommand::{Clock, Focus}; - match TransportCommand::input_to_command(state, input)? { - Clock(_) => { todo!() }, - Focus(command) => Cmd::Focus(command) - } + AppFocus::Menu => { + todo!() }, - AppFocus::Content(ArrangerFocus::PhraseEditor) => { - Cmd::Editor(PhraseCommand::input_to_command(state, input)?) - }, - AppFocus::Content(ArrangerFocus::Phrases) => match input.event() { - key!(KeyCode::Char('e')) => { - Cmd::EditPhrase(state.phrase_editing().clone()) + AppFocus::Content(focused) => match focused { + ArrangerFocus::Transport(_) => { + use TransportCommand::{Clock, Focus}; + match TransportCommand::input_to_command(state, input)? { + Clock(_) => { todo!() }, + Focus(command) => Cmd::Focus(command) + } }, - _ => { - Cmd::Phrases(PhrasesCommand::input_to_command(state, input)?) - } - }, - AppFocus::Content(ArrangerFocus::Arranger) => { - use ArrangerSelection::*; - use KeyCode::Char; - match input.event() { - key!(Char('e')) => Cmd::EditPhrase(state.phrase_editing().clone()), - key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key!(Char('+')) => Cmd::Zoom(0), // TODO - key!(Char('=')) => Cmd::Zoom(0), // TODO - key!(Char('_')) => Cmd::Zoom(0), // TODO - key!(Char('-')) => Cmd::Zoom(0), // TODO - key!(Char('`')) => { todo!("toggle state mode") }, - key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), - _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, + ArrangerFocus::PhraseEditor => { + Cmd::Editor(PhraseCommand::input_to_command(state, input)?) + }, + ArrangerFocus::Phrases => match input.event() { + key!(KeyCode::Char('e')) => { + Cmd::EditPhrase(state.phrase_editing().clone()) + }, + _ => { + Cmd::Phrases(PhrasesCommand::input_to_command(state, input)?) + } + }, + ArrangerFocus::Arranger => { + use ArrangerSelection::*; + use KeyCode::Char; + match input.event() { + key!(Char('e')) => Cmd::EditPhrase(state.phrase_editing().clone()), + key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), + key!(Char('+')) => Cmd::Zoom(0), // TODO + key!(Char('=')) => Cmd::Zoom(0), // TODO + key!(Char('_')) => Cmd::Zoom(0), // TODO + key!(Char('-')) => Cmd::Zoom(0), // TODO + key!(Char('`')) => { todo!("toggle state mode") }, + key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), + key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + _ => match state.selected() { + Mix => to_arranger_mix_command(input)?, + Track(t) => to_arranger_track_command(input, t)?, + Scene(s) => to_arranger_scene_command(input, s)?, + Clip(t, s) => to_arranger_clip_command(input, t, s)?, + } } } } @@ -288,7 +294,10 @@ impl InputToCommand for PhrasesCommand { index + 1, state.phrases()[index].read().unwrap().duplicate() )), - key!(Char('c')) => Self::Phrase(Pool::Color(index, ItemColor::random())), + key!(Char('c')) => Self::Phrase(Pool::SetColor( + index, + ItemColor::random() + )), key!(Char('n')) => Self::Rename(Rename::Begin), key!(Char('t')) => Self::Length(Length::Begin), _ => match state.phrases_mode() { diff --git a/crates/tek_tui/src/tui_view.rs b/crates/tek_tui/src/tui_view.rs index bec8b3b4..7fa597a8 100644 --- a/crates/tek_tui/src/tui_view.rs +++ b/crates/tek_tui/src/tui_view.rs @@ -222,7 +222,7 @@ pub fn arranger_content_vertical ( let name = format!("▎{}", &name[0..max_w]); let name = TuiStyle::bold(name, true); // beats elapsed - let elapsed = if let Some((_, Some(phrase))) = track.phrase().as_ref() { + let elapsed = if let Some((_, Some(phrase))) = track.play_phrase().as_ref() { let length = phrase.read().unwrap().length; let elapsed = track.pulses_since_start().unwrap(); let elapsed = timebase.format_beats_1_short( @@ -282,7 +282,7 @@ pub fn arranger_content_vertical ( let color = phrase.read().unwrap().color; add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?; bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.phrase() { + if let Some((_, Some(ref playing))) = track.play_phrase() { if *playing.read().unwrap() == *phrase.read().unwrap() { bg = color.light.rgb }