launch countdown/switchover, pt.3

This commit is contained in:
🪞👃🪞 2024-11-01 21:28:04 +02:00
parent 97a7bf5b1d
commit 5f112cc203
2 changed files with 75 additions and 50 deletions

View file

@ -1,4 +1,5 @@
use crate::*; use crate::*;
use std::cmp::PartialEq;
/// MIDI message structural /// MIDI message structural
pub type PhraseData = Vec<Vec<MidiMessage>>; pub type PhraseData = Vec<Vec<MidiMessage>>;
/// MIDI message serialized /// MIDI message serialized
@ -23,8 +24,7 @@ pub struct Sequencer<E: Engine> {
pub player: PhrasePlayer, pub player: PhrasePlayer,
} }
/// Sections in the sequencer app that may be focused /// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus {
pub enum SequencerFocus {
/// The transport (toolbar) is focused /// The transport (toolbar) is focused
Transport, Transport,
/// The phrase list (pool) is focused /// The phrase list (pool) is focused
@ -60,8 +60,7 @@ pub enum PhrasePoolMode {
Length(usize, usize, PhraseLengthFocus), Length(usize, usize, PhraseLengthFocus),
} }
/// A MIDI sequence. /// A MIDI sequence.
#[derive(Debug, Clone)] #[derive(Debug, Clone)] pub struct Phrase {
pub struct Phrase {
pub uuid: uuid::Uuid, pub uuid: uuid::Uuid,
/// Name of phrase /// Name of phrase
pub name: String, pub name: String,
@ -141,6 +140,27 @@ pub struct PhrasePlayer {
/// Notes currently held at output /// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>, pub notes_out: Arc<RwLock<[bool; 128]>>,
} }
/// Displays and edits phrase length.
pub struct PhraseLength<E: Engine> {
_engine: PhantomData<E>,
/// 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<PhraseLengthFocus>,
}
/// 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 /// Focus layout of sequencer app
impl<E: Engine> FocusGrid<SequencerFocus> for Sequencer<E> { impl<E: Engine> FocusGrid<SequencerFocus> for Sequencer<E> {
fn cursor (&self) -> (usize, usize) { self.focus_cursor } fn cursor (&self) -> (usize, usize) { self.focus_cursor }
@ -366,9 +386,7 @@ impl Phrase {
impl Default for Phrase { impl Default for Phrase {
fn default () -> Self { Self::new("(empty)", false, 0, None, Some(Color::Rgb(0, 0, 0))) } fn default () -> Self { Self::new("(empty)", false, 0, None, Some(Color::Rgb(0, 0, 0))) }
} }
impl std::cmp::PartialEq for Phrase { impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid }
}
impl Eq for Phrase {} impl Eq for Phrase {}
impl PhrasePlayer { impl PhrasePlayer {
pub fn new (clock: &Arc<TransportTime>) -> Self { pub fn new (clock: &Arc<TransportTime>) -> Self {
@ -404,18 +422,6 @@ impl PhrasePlayer {
.map(|started|(started - self.clock.instant.sample().get()) as usize) .map(|started|(started - self.clock.instant.sample().get()) as usize)
} }
} }
/// Displays and edits phrase length
pub struct PhraseLength<E: Engine> {
_engine: PhantomData<E>,
/// 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<PhraseLengthFocus>,
}
impl<E: Engine> PhraseLength<E> { impl<E: Engine> PhraseLength<E> {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self { pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
@ -427,15 +433,6 @@ impl<E: Engine> PhraseLength<E> {
pub fn beats_string (&self) -> String { format!("{}", self.beats()) } pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } 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 { impl PhraseLengthFocus {
pub fn next (&mut self) { pub fn next (&mut self) {
*self = match self { *self = match self {

View file

@ -1,4 +1,5 @@
use crate::*; use crate::*;
/// JACK process callback for sequencer app
impl<E: Engine> Audio for Sequencer<E> { impl<E: Engine> Audio for Sequencer<E> {
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
if let Some(ref transport) = self.transport { if let Some(ref transport) = self.transport {
@ -8,6 +9,7 @@ impl<E: Engine> Audio for Sequencer<E> {
Control::Continue Control::Continue
} }
} }
/// JACK process callback for a sequencer's phrase player/recorder.
impl Audio for PhrasePlayer { impl Audio for PhrasePlayer {
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
let has_midi_outputs = self.has_midi_outputs(); let has_midi_outputs = self.has_midi_outputs();
@ -32,6 +34,7 @@ impl Audio for PhrasePlayer {
Control::Continue Control::Continue
} }
} }
/// Methods used primarily by the process callback
impl PhrasePlayer { impl PhrasePlayer {
fn has_midi_inputs (&self) -> bool { self.midi_inputs.len() > 0 } fn has_midi_inputs (&self) -> bool { self.midi_inputs.len() > 0 }
fn has_midi_outputs (&self) -> bool { self.midi_outputs.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) { fn play (&mut self, scope: &ProcessScope) {
let sample0 = scope.last_frame_time() as usize; let sample0 = scope.last_frame_time() as usize;
let samples = scope.n_frames() 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() { 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 sample = sample0.saturating_sub(start);
let ticks = self.clock.timebase().pulses_between_samples(sample, sample + samples); // Iterator that emits sample (index into output buffer at which to write MIDI event)
phrase.read().map(|phrase|{ // 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; let output = &mut self.midi_out_buf;
let notes_on = &mut self.notes_out.write().unwrap(); // Buffer for bytes of a MIDI event.
let mut buf = Vec::with_capacity(8); let mut mm = Vec::with_capacity(8);
for (sample, tick) in ticks { // 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, // If a next phrase is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count tick correctly) // break the loop here (FIXME count pulse correctly)
if self.next_phrase.is_some() && tick >= phrase.length { break } if self.next_phrase.is_some() && pulse >= phrase.length { break }
// Output events from phrase at appropriate frames of output buffer let pulse = pulse % phrase.length;
let tick = tick % phrase.length; // Output each MIDI event from phrase at appropriate frames of output buffer:
for message in phrase.notes[tick].iter() { for message in phrase.notes[pulse].iter() {
buf.clear(); // Clear output buffer for this MIDI event.
mm.clear();
// TODO: support MIDI channels other than CH1.
let channel = 0.into(); let channel = 0.into();
let message = *message; // Serialize MIDI event into message buffer.
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap(); LiveEvent::Midi { channel, message: *message }.write(&mut mm).unwrap();
output[sample].push(buf.clone()); // Append serialized message to output buffer.
update_keys(notes_on, &message); 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() { // Handle next enqueued phrase, if any:
// TODO switch to next phrase and fill in remaining ticks from it 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) { fn record (&mut self, scope: &ProcessScope) {