mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
launch pt.9: fix beats_per_second
This commit is contained in:
parent
e149d777ed
commit
e7dce0f84b
4 changed files with 87 additions and 85 deletions
|
|
@ -74,7 +74,7 @@ impl Timebase {
|
|||
}
|
||||
/// Iterate over ticks between start and end.
|
||||
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) } }
|
||||
|
|
@ -126,23 +126,23 @@ impl Instant {
|
|||
}
|
||||
}
|
||||
/// 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 {
|
||||
type Item = (usize, usize);
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.sample > self.end { return None }
|
||||
let fpt = self.fpt;
|
||||
let spp = self.spp;
|
||||
let sample = self.sample as f64;
|
||||
let start = self.start;
|
||||
let end = self.end;
|
||||
self.sample += 1;
|
||||
//println!("{fpt} {sample} {start} {end}");
|
||||
let jitter = sample.rem_euclid(fpt); // ramps
|
||||
let next_jitter = (sample + 1.0).rem_euclid(fpt);
|
||||
//println!("{spp} {sample} {start} {end}");
|
||||
let jitter = sample.rem_euclid(spp); // ramps
|
||||
let next_jitter = (sample + 1.0).rem_euclid(spp);
|
||||
if jitter > next_jitter { // at crossing:
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -173,7 +173,7 @@ pub trait MIDITime {
|
|||
/// Return the duration fo a beat in microseconds
|
||||
#[inline] fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm().get() }
|
||||
/// 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
|
||||
#[inline] fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
|
||||
4.0 * self.usec_per_beat() * num / den
|
||||
|
|
|
|||
|
|
@ -170,15 +170,16 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
|||
}))?;
|
||||
// track titles
|
||||
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 max_w = w.saturating_sub(1).min(name.len()).max(2);
|
||||
let name = format!("▎{}", &name[0..max_w]);
|
||||
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())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into());
|
||||
let input = format!("▎>{}", input_name);
|
||||
.unwrap_or("(none)".into()));
|
||||
col!(name, input)
|
||||
.min_xy(w as u16, track_title_h)
|
||||
.bg(track.color)
|
||||
|
|
@ -187,6 +188,7 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
|||
// track controls
|
||||
let footer = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
|
||||
let player = &track.player;
|
||||
// beats elapsed
|
||||
let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
let elapsed = player.pulses_since_start().unwrap();
|
||||
|
|
@ -197,23 +199,24 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
|||
} else {
|
||||
String::from("▎")
|
||||
};
|
||||
// beats until switchover
|
||||
let until_next = player.next_phrase.as_ref()
|
||||
.map(|(t, _)|{
|
||||
let target = t.pulse().get();
|
||||
let current = clock.instant.pulse().get();
|
||||
if target > current {
|
||||
let remaining = clock.timebase().format_beats_0_short(target - current);
|
||||
format!("▎-{remaining:>}")
|
||||
let remaining = target - current;
|
||||
format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
.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())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into());
|
||||
let output = format!("▎<{}", output_name);
|
||||
.unwrap_or("(none)".into()));
|
||||
col!(until_next, elapsed, output)
|
||||
.min_xy(w as u16, tracks_footer)
|
||||
.bg(track.color)
|
||||
|
|
|
|||
|
|
@ -416,15 +416,12 @@ impl PhrasePlayer {
|
|||
));
|
||||
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> {
|
||||
self.phrase.as_ref()
|
||||
.map(|(started,_)|started.pulse().get())
|
||||
.map(|started|self.clock.instant.pulse().get() - started)
|
||||
if let Some((started, Some(_))) = self.phrase.as_ref() {
|
||||
Some(self.clock.instant.pulse().get() - started.pulse().get())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: Engine> PhraseLength<E> {
|
||||
|
|
|
|||
|
|
@ -52,72 +52,74 @@ impl PhrasePlayer {
|
|||
fn play (&mut self, scope: &ProcessScope) {
|
||||
let sample0 = scope.last_frame_time() 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
|
||||
if let (true, Some((started, phrase))) = (self.is_rolling(), &self.phrase) {
|
||||
// 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(started.sample.get() as usize);
|
||||
// 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);
|
||||
// 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)
|
||||
next = self.next_phrase.is_some() && if let Some(ref phrase) = phrase {
|
||||
pulse >= phrase.read().unwrap().length
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if next {
|
||||
break
|
||||
}
|
||||
// If there's a currently playing phrase, output notes from it to buffer:
|
||||
if let Some(ref phrase) = phrase {
|
||||
// Source phrase from which the MIDI events will be taken.
|
||||
let phrase = phrase.read().unwrap();
|
||||
// Current pulse index in source phrase
|
||||
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);
|
||||
if self.is_rolling() {
|
||||
// If no phrase is playing, prepare for switchover immediately
|
||||
let mut next = self.phrase.is_none();
|
||||
if let Some((started, phrase)) = &self.phrase {
|
||||
// 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(started.sample.get() as usize);
|
||||
// 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);
|
||||
// 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)
|
||||
next = self.next_phrase.is_some() && if let Some(ref phrase) = phrase {
|
||||
pulse >= phrase.read().unwrap().length
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if next {
|
||||
break
|
||||
}
|
||||
// If there's a currently playing phrase, output notes from it to buffer:
|
||||
if let Some(ref phrase) = phrase {
|
||||
// Source phrase from which the MIDI events will be taken.
|
||||
let phrase = phrase.read().unwrap();
|
||||
// Current pulse index in source phrase
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If it's time for the next enqueued phrase, handle it here:
|
||||
if next {
|
||||
if let (true, Some((start_at, phrase))) = (self.is_rolling(), &self.next_phrase) {
|
||||
let start = start_at.sample().get() as usize;
|
||||
// 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, phrase.clone()));
|
||||
// Unset enqueuement (TODO: where to implement looping?)
|
||||
self.next_phrase = None
|
||||
// If it's time for the next enqueued phrase, handle it here:
|
||||
if next {
|
||||
if let Some((start_at, phrase)) = &self.next_phrase {
|
||||
let start = start_at.sample().get() as usize;
|
||||
// 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, phrase.clone()));
|
||||
// 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 ???
|
||||
}
|
||||
// TODO fill in remaining ticks of chunk from next phrase.
|
||||
// ?? just call self.play(scope) again, since enqueuement is off ???
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue