mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 20:26: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.
|
/// 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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 ???
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue