launch pt.9: fix beats_per_second

This commit is contained in:
🪞👃🪞 2024-11-01 23:59:39 +02:00
parent e149d777ed
commit e7dce0f84b
4 changed files with 87 additions and 85 deletions

View file

@ -74,7 +74,7 @@ impl Timebase {
} }
/// Iterate over ticks between start and end. /// Iterate over ticks between start and end.
pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator {
TicksIterator { fpt: self.samples_per_pulse(), sample: start, start, end } TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end }
} }
} }
impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } } impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } }
@ -126,23 +126,23 @@ impl Instant {
} }
} }
/// Iterator that emits subsequent ticks within a range. /// Iterator that emits subsequent ticks within a range.
pub struct TicksIterator { fpt: f64, sample: usize, start: usize, end: usize, } pub struct TicksIterator { spp: f64, sample: usize, start: usize, end: usize, }
impl Iterator for TicksIterator { impl Iterator for TicksIterator {
type Item = (usize, usize); type Item = (usize, usize);
fn next (&mut self) -> Option<Self::Item> { fn next (&mut self) -> Option<Self::Item> {
loop { loop {
if self.sample > self.end { return None } if self.sample > self.end { return None }
let fpt = self.fpt; let spp = self.spp;
let sample = self.sample as f64; let sample = self.sample as f64;
let start = self.start; let start = self.start;
let end = self.end; let end = self.end;
self.sample += 1; self.sample += 1;
//println!("{fpt} {sample} {start} {end}"); //println!("{spp} {sample} {start} {end}");
let jitter = sample.rem_euclid(fpt); // ramps let jitter = sample.rem_euclid(spp); // ramps
let next_jitter = (sample + 1.0).rem_euclid(fpt); let next_jitter = (sample + 1.0).rem_euclid(spp);
if jitter > next_jitter { // at crossing: if jitter > next_jitter { // at crossing:
let time = (sample as usize) % (end as usize-start as usize); let time = (sample as usize) % (end as usize-start as usize);
let tick = (sample / fpt) as usize; let tick = (sample / spp) as usize;
return Some((time, tick)) return Some((time, tick))
} }
} }
@ -173,7 +173,7 @@ pub trait MIDITime {
/// Return the duration fo a beat in microseconds /// Return the duration fo a beat in microseconds
#[inline] fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm().get() } #[inline] fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm().get() }
/// Return the number of beats in a second /// Return the number of beats in a second
#[inline] fn beat_per_second (&self) -> f64 { self.bpm().get() / 60_000_000f64 } #[inline] fn beat_per_second (&self) -> f64 { self.bpm().get() / 60f64 }
/// Return the number of microseconds corresponding to a note of the given duration /// Return the number of microseconds corresponding to a note of the given duration
#[inline] fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { #[inline] fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
4.0 * self.usec_per_beat() * num / den 4.0 * self.usec_per_beat() * num / den

View file

@ -170,15 +170,16 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
}))?; }))?;
// track titles // track titles
let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
// name and width of track
let name = track.name.read().unwrap(); let name = track.name.read().unwrap();
let max_w = w.saturating_sub(1).min(name.len()).max(2); let max_w = w.saturating_sub(1).min(name.len()).max(2);
let name = format!("{}", &name[0..max_w]); let name = format!("{}", &name[0..max_w]);
let name = TuiStyle::bold(name, true); let name = TuiStyle::bold(name, true);
let input_name = track.player.midi_inputs.get(0) // name of active MIDI input
let input = format!("▎>{}", track.player.midi_inputs.get(0)
.map(|port|port.short_name()) .map(|port|port.short_name())
.transpose()? .transpose()?
.unwrap_or("(none)".into()); .unwrap_or("(none)".into()));
let input = format!("▎>{}", input_name);
col!(name, input) col!(name, input)
.min_xy(w as u16, track_title_h) .min_xy(w as u16, track_title_h)
.bg(track.color) .bg(track.color)
@ -187,6 +188,7 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
// track controls // track controls
let footer = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ let footer = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
let player = &track.player; let player = &track.player;
// beats elapsed
let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() { let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() {
let length = phrase.read().unwrap().length; let length = phrase.read().unwrap().length;
let elapsed = player.pulses_since_start().unwrap(); let elapsed = player.pulses_since_start().unwrap();
@ -197,23 +199,24 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
} else { } else {
String::from("") String::from("")
}; };
// beats until switchover
let until_next = player.next_phrase.as_ref() let until_next = player.next_phrase.as_ref()
.map(|(t, _)|{ .map(|(t, _)|{
let target = t.pulse().get(); let target = t.pulse().get();
let current = clock.instant.pulse().get(); let current = clock.instant.pulse().get();
if target > current { if target > current {
let remaining = clock.timebase().format_beats_0_short(target - current); let remaining = target - current;
format!("▎-{remaining:>}") format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining))
} else { } else {
String::new() String::new()
} }
}) })
.unwrap_or(String::from("")); .unwrap_or(String::from(""));
let output_name = track.player.midi_outputs.get(0) // name of active MIDI output
let output = format!("▎<{}", track.player.midi_outputs.get(0)
.map(|port|port.short_name()) .map(|port|port.short_name())
.transpose()? .transpose()?
.unwrap_or("(none)".into()); .unwrap_or("(none)".into()));
let output = format!("▎<{}", output_name);
col!(until_next, elapsed, output) col!(until_next, elapsed, output)
.min_xy(w as u16, tracks_footer) .min_xy(w as u16, tracks_footer)
.bg(track.color) .bg(track.color)

View file

@ -416,15 +416,12 @@ impl PhrasePlayer {
)); ));
self.reset = true; self.reset = true;
} }
pub fn samples_since_start (&self) -> Option<usize> {
self.phrase.as_ref()
.map(|(started,_)|started.sample().get())
.map(|started|(self.clock.instant.sample().get() - started) as usize)
}
pub fn pulses_since_start (&self) -> Option<f64> { pub fn pulses_since_start (&self) -> Option<f64> {
self.phrase.as_ref() if let Some((started, Some(_))) = self.phrase.as_ref() {
.map(|(started,_)|started.pulse().get()) Some(self.clock.instant.pulse().get() - started.pulse().get())
.map(|started|self.clock.instant.pulse().get() - started) } else {
None
}
} }
} }
impl<E: Engine> PhraseLength<E> { impl<E: Engine> PhraseLength<E> {

View file

@ -52,72 +52,74 @@ 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;
// If no phrase is playing, prepare for switchover immediately
let mut next = self.phrase.is_none();
// Write MIDI events from currently playing phrase (if any) to MIDI output buffer // Write MIDI events from currently playing phrase (if any) to MIDI output buffer
if let (true, Some((started, phrase))) = (self.is_rolling(), &self.phrase) { if self.is_rolling() {
// First sample to populate. Greater than 0 means that the first // If no phrase is playing, prepare for switchover immediately
// pulse of the phrase falls somewhere in the middle of the chunk. let mut next = self.phrase.is_none();
let sample = sample0.saturating_sub(started.sample.get() as usize); if let Some((started, phrase)) = &self.phrase {
// Iterator that emits sample (index into output buffer at which to write MIDI event) // First sample to populate. Greater than 0 means that the first
// paired with pulse (index into phrase from which to take the MIDI event) for each // pulse of the phrase falls somewhere in the middle of the chunk.
// sample of the output buffer that corresponds to a MIDI pulse. let sample = sample0.saturating_sub(started.sample.get() as usize);
let pulses = self.clock.timebase().pulses_between_samples(sample, sample + samples); // Iterator that emits sample (index into output buffer at which to write MIDI event)
// MIDI output buffer that will be copied to the JACK MIDI output ports. // paired with pulse (index into phrase from which to take the MIDI event) for each
let output = &mut self.midi_out_buf; // sample of the output buffer that corresponds to a MIDI pulse.
// Buffer for bytes of a MIDI event. let pulses = self.clock.timebase().pulses_between_samples(sample, sample + samples);
let mut mm = Vec::with_capacity(8); // MIDI output buffer that will be copied to the JACK MIDI output ports.
// Notes active during current chunk. let output = &mut self.midi_out_buf;
let notes = &mut self.notes_out.write().unwrap(); // Buffer for bytes of a MIDI event.
for (sample, pulse) in pulses { let mut mm = Vec::with_capacity(8);
// If a next phrase is enqueued, and we're past the end of the current one, // Notes active during current chunk.
// break the loop here (FIXME count pulse correctly) let notes = &mut self.notes_out.write().unwrap();
next = self.next_phrase.is_some() && if let Some(ref phrase) = phrase { for (sample, pulse) in pulses {
pulse >= phrase.read().unwrap().length // If a next phrase is enqueued, and we're past the end of the current one,
} else { // break the loop here (FIXME count pulse correctly)
true next = self.next_phrase.is_some() && if let Some(ref phrase) = phrase {
}; pulse >= phrase.read().unwrap().length
if next { } else {
break true
} };
// If there's a currently playing phrase, output notes from it to buffer: if next {
if let Some(ref phrase) = phrase { break
// Source phrase from which the MIDI events will be taken. }
let phrase = phrase.read().unwrap(); // If there's a currently playing phrase, output notes from it to buffer:
// Current pulse index in source phrase if let Some(ref phrase) = phrase {
let pulse = pulse % phrase.length; // Source phrase from which the MIDI events will be taken.
// Output each MIDI event from phrase at appropriate frames of output buffer: let phrase = phrase.read().unwrap();
for message in phrase.notes[pulse].iter() { // Current pulse index in source phrase
// Clear output buffer for this MIDI event. let pulse = pulse % phrase.length;
mm.clear(); // Output each MIDI event from phrase at appropriate frames of output buffer:
// TODO: support MIDI channels other than CH1. for message in phrase.notes[pulse].iter() {
let channel = 0.into(); // Clear output buffer for this MIDI event.
// Serialize MIDI event into message buffer. mm.clear();
LiveEvent::Midi { channel, message: *message }.write(&mut mm).unwrap(); // TODO: support MIDI channels other than CH1.
// Append serialized message to output buffer. let channel = 0.into();
output[sample].push(mm.clone()); // Serialize MIDI event into message buffer.
// Update the list of currently held notes. LiveEvent::Midi { channel, message: *message }.write(&mut mm).unwrap();
update_keys(notes, &message); // Append serialized message to output buffer.
output[sample].push(mm.clone());
// Update the list of currently held notes.
update_keys(notes, &message);
}
} }
} }
} }
} // If it's time for the next enqueued phrase, handle it here:
// If it's time for the next enqueued phrase, handle it here: if next {
if next { if let Some((start_at, phrase)) = &self.next_phrase {
if let (true, Some((start_at, phrase))) = (self.is_rolling(), &self.next_phrase) { let start = start_at.sample().get() as usize;
let start = start_at.sample().get() as usize; // If it's time to switch to the next phrase:
// If it's time to switch to the next phrase: if start <= sample0 {
if start <= sample0 { // Samples elapsed since phrase was supposed to start
// Samples elapsed since phrase was supposed to start let skipped = sample0 - start;
let skipped = sample0 - start; // Switch over to enqueued phrase
// Switch over to enqueued phrase let started = Instant::from_sample(&self.clock.timebase(), start as f64);
let started = Instant::from_sample(&self.clock.timebase(), start as f64); self.phrase = Some((started, phrase.clone()));
self.phrase = Some((started, phrase.clone())); // Unset enqueuement (TODO: where to implement looping?)
// Unset enqueuement (TODO: where to implement looping?) self.next_phrase = None
self.next_phrase = None }
// TODO fill in remaining ticks of chunk from next phrase.
// ?? just call self.play(scope) again, since enqueuement is off ???
} }
// TODO fill in remaining ticks of chunk from next phrase.
// ?? just call self.play(scope) again, since enqueuement is off ???
} }
} }
} }