diff --git a/crates/tek_sequencer/src/sequencer.rs b/crates/tek_sequencer/src/sequencer.rs index a8654cd2..936b0290 100644 --- a/crates/tek_sequencer/src/sequencer.rs +++ b/crates/tek_sequencer/src/sequencer.rs @@ -1,4 +1,5 @@ use crate::*; +use std::cmp::PartialEq; /// MIDI message structural pub type PhraseData = Vec>; /// MIDI message serialized @@ -23,8 +24,7 @@ pub struct Sequencer { pub player: PhrasePlayer, } /// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum SequencerFocus { +#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus { /// The transport (toolbar) is focused Transport, /// The phrase list (pool) is focused @@ -60,8 +60,7 @@ pub enum PhrasePoolMode { Length(usize, usize, PhraseLengthFocus), } /// A MIDI sequence. -#[derive(Debug, Clone)] -pub struct Phrase { +#[derive(Debug, Clone)] pub struct Phrase { pub uuid: uuid::Uuid, /// Name of phrase pub name: String, @@ -141,6 +140,27 @@ pub struct PhrasePlayer { /// Notes currently held at output pub notes_out: Arc>, } +/// Displays and edits phrase length. +pub struct PhraseLength { + _engine: PhantomData, + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of phrase in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} +/// Focused field of `PhraseLength` +#[derive(Copy, Clone)] pub enum PhraseLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} /// Focus layout of sequencer app impl FocusGrid for Sequencer { fn cursor (&self) -> (usize, usize) { self.focus_cursor } @@ -366,9 +386,7 @@ impl Phrase { impl Default for Phrase { fn default () -> Self { Self::new("(empty)", false, 0, None, Some(Color::Rgb(0, 0, 0))) } } -impl std::cmp::PartialEq for Phrase { - fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } -} +impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } impl Eq for Phrase {} impl PhrasePlayer { pub fn new (clock: &Arc) -> Self { @@ -404,18 +422,6 @@ impl PhrasePlayer { .map(|started|(started - self.clock.instant.sample().get()) as usize) } } -/// Displays and edits phrase length -pub struct PhraseLength { - _engine: PhantomData, - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} impl PhraseLength { pub fn new (pulses: usize, focus: Option) -> Self { Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } @@ -427,15 +433,6 @@ impl PhraseLength { pub fn beats_string (&self) -> String { format!("{}", self.beats()) } pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } } -#[derive(Copy,Clone)] -pub enum PhraseLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} impl PhraseLengthFocus { pub fn next (&mut self) { *self = match self { diff --git a/crates/tek_sequencer/src/sequencer_snd.rs b/crates/tek_sequencer/src/sequencer_snd.rs index b3e6ab4b..4f555e56 100644 --- a/crates/tek_sequencer/src/sequencer_snd.rs +++ b/crates/tek_sequencer/src/sequencer_snd.rs @@ -1,4 +1,5 @@ use crate::*; +/// JACK process callback for sequencer app impl Audio for Sequencer { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { if let Some(ref transport) = self.transport { @@ -8,6 +9,7 @@ impl Audio for Sequencer { Control::Continue } } +/// JACK process callback for a sequencer's phrase player/recorder. impl Audio for PhrasePlayer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { let has_midi_outputs = self.has_midi_outputs(); @@ -32,6 +34,7 @@ impl Audio for PhrasePlayer { Control::Continue } } +/// Methods used primarily by the process callback impl PhrasePlayer { fn has_midi_inputs (&self) -> bool { self.midi_inputs.len() > 0 } fn has_midi_outputs (&self) -> bool { self.midi_outputs.len() > 0 } @@ -67,32 +70,57 @@ impl PhrasePlayer { fn play (&mut self, scope: &ProcessScope) { let sample0 = scope.last_frame_time() as usize; let samples = scope.n_frames() as usize; + // Write MIDI events from currently playing phrase (if any) to MIDI output buffer if let Some((start, ref phrase)) = self.playing() { + // First sample to populate. Greater than 0 means that the first + // pulse of the phrase falls somewhere in the middle of the chunk. let sample = sample0.saturating_sub(start); - let ticks = self.clock.timebase().pulses_between_samples(sample, sample + samples); - phrase.read().map(|phrase|{ - let output = &mut self.midi_out_buf; - let notes_on = &mut self.notes_out.write().unwrap(); - let mut buf = Vec::with_capacity(8); - for (sample, tick) in ticks { - // If a next phrase is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count tick correctly) - if self.next_phrase.is_some() && tick >= phrase.length { break } - // Output events from phrase at appropriate frames of output buffer - let tick = tick % phrase.length; - for message in phrase.notes[tick].iter() { - buf.clear(); - let channel = 0.into(); - let message = *message; - LiveEvent::Midi { channel, message }.write(&mut buf).unwrap(); - output[sample].push(buf.clone()); - update_keys(notes_on, &message); - } + // Iterator that emits sample (index into output buffer at which to write MIDI event) + // paired with pulse (index into phrase from which to take the MIDI event) for each + // sample of the output buffer that corresponds to a MIDI pulse. + let pulses = self.clock.timebase().pulses_between_samples(sample, sample + samples); + // MIDI output buffer that will be copied to the JACK MIDI output ports. + let output = &mut self.midi_out_buf; + // Buffer for bytes of a MIDI event. + let mut mm = Vec::with_capacity(8); + // Source phrase from which the MIDI events will be taken. + let phrase = phrase.read().unwrap(); + // Notes active during current chunk. + let notes = &mut self.notes_out.write().unwrap(); + for (sample, pulse) in pulses { + // If a next phrase is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + if self.next_phrase.is_some() && pulse >= phrase.length { break } + let pulse = pulse % phrase.length; + // Output each MIDI event from phrase at appropriate frames of output buffer: + for message in phrase.notes[pulse].iter() { + // Clear output buffer for this MIDI event. + mm.clear(); + // TODO: support MIDI channels other than CH1. + let channel = 0.into(); + // Serialize MIDI event into message buffer. + LiveEvent::Midi { channel, message: *message }.write(&mut mm).unwrap(); + // Append serialized message to output buffer. + output[sample].push(mm.clone()); + // Update the list of currently held notes. + update_keys(notes, &message); } - }).unwrap() + } } - if let Some((start, ref phrase)) = self.enqueued() { - // TODO switch to next phrase and fill in remaining ticks from it + // Handle next enqueued phrase, if any: + if let Some((start, phrase)) = self.enqueued() { + // If it's time to switch to the next phrase: + if start <= sample0 { + // Samples elapsed since phrase was supposed to start + let skipped = sample0 - start; + // Switch over to enqueued phrase + let started = Instant::from_sample(&self.clock.timebase(), start as f64); + self.phrase = Some((started, Some(phrase))); + // Unset enqueuement (TODO: where to implement looping?) + self.next_phrase = None + } + // TODO fill in remaining ticks of chunk from next phrase. + // ?? just call self.play(scope) again, since enqueuement is off ??? } } fn record (&mut self, scope: &ProcessScope) {