mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
stack modes
This commit is contained in:
parent
9e3cc1139d
commit
f321397d6f
11 changed files with 570 additions and 498 deletions
|
|
@ -1,4 +1,6 @@
|
|||
use crate::*;
|
||||
use ::std::sync::Arc;
|
||||
use ::atomic_float::AtomicF64;
|
||||
|
||||
impl App {
|
||||
/// Update memoized render of clock values.
|
||||
|
|
@ -363,79 +365,312 @@ impl_time_unit!(Pulse);
|
|||
impl_time_unit!(Bpm);
|
||||
impl_time_unit!(LaunchSync);
|
||||
|
||||
/// Contains memoized renders of clock values.
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||
///
|
||||
/// Performance optimization.
|
||||
#[derive(Debug)] pub struct ClockView {
|
||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||
pub buf: Memo<Option<f64>, String>,
|
||||
pub lat: Memo<Option<f64>, String>,
|
||||
pub bpm: Memo<Option<f64>, String>,
|
||||
pub beat: Memo<Option<f64>, String>,
|
||||
pub time: Memo<Option<f64>, String>,
|
||||
/// ```
|
||||
/// let _ = tek::Timebase::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct Timebase {
|
||||
/// Audio samples per second
|
||||
pub sr: SampleRate,
|
||||
/// MIDI beats per minute
|
||||
pub bpm: Bpm,
|
||||
/// MIDI ticks per beat
|
||||
pub ppq: Ppq,
|
||||
}
|
||||
|
||||
impl_default!(ClockView: {
|
||||
let mut beat = String::with_capacity(16);
|
||||
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
|
||||
let mut time = String::with_capacity(16);
|
||||
let _ = write!(time, "{}", Self::TIME_EMPTY);
|
||||
let mut bpm = String::with_capacity(16);
|
||||
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
|
||||
/// Audio sample rate in Hz (samples per second)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
||||
|
||||
/// Tempo in beats per minute
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64);
|
||||
|
||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64);
|
||||
|
||||
impl Timebase {
|
||||
/// Specify sample rate, BPM and PPQ
|
||||
pub fn new (
|
||||
s: impl Into<SampleRate>,
|
||||
b: impl Into<Bpm>,
|
||||
p: impl Into<Ppq>
|
||||
) -> Self {
|
||||
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
|
||||
}
|
||||
/// Iterate over ticks between start and end.
|
||||
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker {
|
||||
Ticker { spp: self.samples_per_pulse(), sample: start, start, end }
|
||||
}
|
||||
/// Return the duration fo a beat in microseconds
|
||||
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
|
||||
/// Return the number of beats in a second
|
||||
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
|
||||
/// Return the number of microseconds corresponding to a note of the given duration
|
||||
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
|
||||
4.0 * self.usec_per_beat() * num / den
|
||||
}
|
||||
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
|
||||
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
|
||||
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
|
||||
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
|
||||
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
|
||||
/// Return number of pulses in a second (BPM-dependent)
|
||||
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
|
||||
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_per_sample (&self) -> f64 {
|
||||
self.usec_per_pulse() / self.sr.usec_per_sample()
|
||||
}
|
||||
/// Return number of samples in a pulse (SR- and BPM-dependent)
|
||||
#[inline] pub fn samples_per_pulse (&self) -> f64 {
|
||||
self.sr.get() / self.pulses_per_second()
|
||||
}
|
||||
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
|
||||
self.pulses_per_sample() * p
|
||||
}
|
||||
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
|
||||
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
|
||||
s / self.pulses_per_sample()
|
||||
}
|
||||
/// Return the number of samples corresponding to a note of the given duration
|
||||
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
|
||||
self.usec_to_sample(self.note_to_usec(note))
|
||||
}
|
||||
/// Return the number of samples corresponding to the given number of microseconds
|
||||
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
|
||||
usec * self.sr.get() / 1000f64
|
||||
}
|
||||
/// Return the quantized position of a moment in time given a step
|
||||
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
|
||||
let step = self.note_to_usec(step);
|
||||
(time / step, time % step)
|
||||
}
|
||||
/// Quantize a collection of events
|
||||
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
|
||||
&self, step: (f64, f64), events: E
|
||||
) -> Vec<(f64, f64)> {
|
||||
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 0
|
||||
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc<str> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar starting from 0
|
||||
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||
format!("{}.{}", beats / 4, beats % 4).into()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
|
||||
let mut string = String::with_capacity(16);
|
||||
self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat");
|
||||
string.into()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1)
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||
format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleRate {
|
||||
/// Return the duration of a sample in microseconds (floating)
|
||||
#[inline] pub fn usec_per_sample (&self) -> f64 {
|
||||
1_000_000f64 / self.get()
|
||||
}
|
||||
/// Return the duration of a sample in microseconds (floating)
|
||||
#[inline] pub fn sample_per_usec (&self) -> f64 {
|
||||
self.get() / 1_000_000f64
|
||||
}
|
||||
/// Convert a number of samples to microseconds (floating)
|
||||
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
|
||||
self.usec_per_sample() * samples
|
||||
}
|
||||
/// Convert a number of microseconds to samples (floating)
|
||||
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
|
||||
self.sample_per_usec() * usecs
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Moment::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)] pub struct Moment {
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current time in microseconds
|
||||
pub usec: Microsecond,
|
||||
/// Current time in audio samples
|
||||
pub sample: SampleCount,
|
||||
/// Current time in MIDI pulses
|
||||
pub pulse: Pulse,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Moment2::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
||||
#[default] None,
|
||||
Zero,
|
||||
Usec(Microsecond),
|
||||
Sample(SampleCount),
|
||||
Pulse(Pulse),
|
||||
}
|
||||
|
||||
/// Timestamp in microseconds
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Microsecond(0.0.into());
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Microsecond (pub AtomicF64);
|
||||
|
||||
/// Timestamp in audio samples
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::SampleCount(0.0.into());
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleCount (pub AtomicF64);
|
||||
|
||||
/// Timestamp in MIDI pulses
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Pulse(0.0.into());
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Pulse (pub AtomicF64);
|
||||
|
||||
impl Moment {
|
||||
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
||||
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
||||
}
|
||||
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
|
||||
Self {
|
||||
beat: Memo::new(None, beat),
|
||||
time: Memo::new(None, time),
|
||||
bpm: Memo::new(None, bpm),
|
||||
sr: Memo::new(None, String::with_capacity(16)),
|
||||
buf: Memo::new(None, String::with_capacity(16)),
|
||||
lat: Memo::new(None, String::with_capacity(16)),
|
||||
}
|
||||
});
|
||||
|
||||
impl ClockView {
|
||||
pub const BEAT_EMPTY: &'static str = "-.-.--";
|
||||
pub const TIME_EMPTY: &'static str = "-.---s";
|
||||
pub const BPM_EMPTY: &'static str = "---.---";
|
||||
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
|
||||
let rate = clock.timebase.sr.get();
|
||||
let chunk = clock.chunk.load(Relaxed) as f64;
|
||||
let lat = chunk / rate * 1000.;
|
||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||
let mut cache = cache.try_write().unwrap();
|
||||
|
||||
cache.buf.update(
|
||||
Some(chunk), rewrite!(buf, "{chunk}")
|
||||
);
|
||||
|
||||
cache.lat.update(
|
||||
Some(lat), rewrite!(buf, "{lat:.1}ms")
|
||||
);
|
||||
|
||||
cache.sr.update(
|
||||
Some((compact, rate)), |buf: &mut String,_,_|{
|
||||
buf.clear();
|
||||
if compact {
|
||||
write!(buf, "{:.1}kHz", rate / 1000.)
|
||||
} else {
|
||||
write!(buf, "{:.0}Hz", rate)
|
||||
usec: usec.into(),
|
||||
sample: timebase.sr.usecs_to_sample(usec).into(),
|
||||
pulse: timebase.usecs_to_pulse(usec).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
);
|
||||
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
|
||||
Self {
|
||||
sample: sample.into(),
|
||||
usec: timebase.sr.samples_to_usec(sample).into(),
|
||||
pulse: timebase.samples_to_pulse(sample).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
|
||||
Self {
|
||||
pulse: pulse.into(),
|
||||
sample: timebase.pulses_to_sample(pulse).into(),
|
||||
usec: timebase.pulses_to_usec(pulse).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
#[inline] pub fn update_from_usec (&self, usec: f64) {
|
||||
self.usec.set(usec);
|
||||
self.pulse.set(self.timebase.usecs_to_pulse(usec));
|
||||
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
|
||||
}
|
||||
#[inline] pub fn update_from_sample (&self, sample: f64) {
|
||||
self.usec.set(self.timebase.sr.samples_to_usec(sample));
|
||||
self.pulse.set(self.timebase.samples_to_pulse(sample));
|
||||
self.sample.set(sample);
|
||||
}
|
||||
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
|
||||
self.usec.set(self.timebase.pulses_to_usec(pulse));
|
||||
self.pulse.set(pulse);
|
||||
self.sample.set(self.timebase.pulses_to_sample(pulse));
|
||||
}
|
||||
#[inline] pub fn format_beat (&self) -> Arc<str> {
|
||||
self.timebase.format_beats_1(self.pulse.get()).into()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
|
||||
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||
let time = now/1000000.;
|
||||
let bpm = clock.timebase.bpm.get();
|
||||
cache.beat.update(Some(pulse), |buf, _, _|{
|
||||
buf.clear();
|
||||
clock.timebase.format_beats_1_to(buf, pulse)
|
||||
});
|
||||
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
|
||||
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
|
||||
} else {
|
||||
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
|
||||
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
|
||||
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
|
||||
impl Microsecond {
|
||||
#[inline] pub fn format_msu (&self) -> Arc<str> {
|
||||
let usecs = self.get() as usize;
|
||||
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||
format!("{minutes}:{seconds:02}:{msecs:03}").into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
///
|
||||
/// ```
|
||||
/// let iter = tek::Ticker::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Ticker {
|
||||
pub spp: f64,
|
||||
pub sample: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Iterator for Ticker {
|
||||
type Item = (usize, usize);
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.sample > self.end { return None }
|
||||
let spp = self.spp;
|
||||
let sample = self.sample as f64;
|
||||
let start = self.start;
|
||||
let end = self.end;
|
||||
self.sample += 1;
|
||||
//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 / spp) as usize;
|
||||
return Some((time, tick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/tek.edn
47
src/tek.edn
|
|
@ -1,8 +1,38 @@
|
|||
(mode Menu (name Main Menu) (info Welcome to Tek!) (keys Y Confirm)
|
||||
(view (bar/s 2 Transport (bar/n 2 StatusBar (align/c (bar/s 2 (align/x Logo) Menu))))))
|
||||
|
||||
(mode Help
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Nobody Will Help! ))))))))))
|
||||
|
||||
(mode Quit
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Are you sure? ))))))))))
|
||||
|
||||
(mode Save
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Select save path ))))))))))
|
||||
|
||||
(mode Load
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Select load path ))))))))))
|
||||
|
||||
(mode History
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Undo/redo history ))))))))))
|
||||
|
||||
(mode Rename
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Rename item ))))))))))
|
||||
|
||||
(mode Options
|
||||
(view (bg (g 32) (fg (g 64) (fill/xy
|
||||
(bg (g 48) (fg (g 224) (align/c (expand/wh 2 2 (text Options ))))))))))
|
||||
|
||||
(mode Transport (name Transport) (info JACK transport controller.)
|
||||
(keys Clock Global)
|
||||
(view Transport))
|
||||
|
||||
(mode Arranger (name Arranger) (info Launch grid.)
|
||||
(keys Global Clock Tracks Scenes Clip Launch Color History)
|
||||
(view (bar/n 2 StatusBar (bar/s 8 (bar/e 16 (align/nw Transport)
|
||||
|
|
@ -14,12 +44,15 @@
|
|||
(bar/s 2 TracksDevicesCells
|
||||
(bar/s 2 TracksNamesCells TracksInputsCells))))))
|
||||
(bar/w 20 (text) (bar/e 16 ScenesNames ScenesClips))))))
|
||||
|
||||
(mode Sequencer (info MIDI sequencer.)
|
||||
(keys Global Clock I IFine Y Page EditorView EditorAdd EditorDel)
|
||||
(view (bsp/s (exact/h 1 Transport) (bsp/n (exact/h 1 StatusBar) (fill/xy (bsp/a (fill/xy (align/e Pool)) Editor))))))
|
||||
|
||||
(mode Sampler (info Sample player.)
|
||||
(keys SamplerDirs SamplerRec SamplerPlay)
|
||||
(view (bsp/s (exact/h 1 Transport) (bsp/n (exact/h 1 StatusBar) (fill/xy SampleGrid)))))
|
||||
|
||||
(mode Groovebox (info Sequencer with sampler.)
|
||||
(keys Global Clock Sampler I IFine Y Page EditorView EditorAdd EditorDel)
|
||||
(view (bsp/w MetersOut (bsp/e MetersIn (bsp/w GrooveMeta GrooveEditor)))))
|
||||
|
|
@ -32,20 +65,22 @@
|
|||
(view GrooveEditor (bsp/n GrooveSample GrooveSequence))
|
||||
(view GrooveSample (exact/h SampleHeight (bsp/e (fill/y (exact/w 20 (align/nw SampleStatus))) SampleViewer)))
|
||||
(view GrooveSequence (bsp/e (fill/y (align/n (bsp/s Status/v EditorStatus))) (bsp/e SamplesKeys Editor)))
|
||||
(view StatusBar (bar/e 40 (bg (g 10) (full/x (pad/x 1 (bar/s 1 (text Selected) Selected))))
|
||||
(bg (g 20) (full/x (pad/x 1 (bar/s 1 (text Mode) ModeList))))))
|
||||
(view Transport (bg (g 0) (pad/x 1 (bar/e 4 PlayPause (bar/e 16 Bpm (bar/e 16 Beat Time))))))
|
||||
(view StatusBar (bar/e 24 (bg (g 50) (full/x (pad/x 1 (bar/s 1 (text Selected) Selected))))
|
||||
(bar/e 24 (bg (g 40) (full/x (pad/x 1 (bar/s 1 (text Mode) ModeList))))
|
||||
(bg (g 30) (full/x (pad/x 1 (bar/s 1 (text History) HistoryCount)))))))
|
||||
|
||||
(keys Global (@f1 (enter Help))
|
||||
(@f2 (project new))
|
||||
(@f3 (enter History))
|
||||
(keys Global (@escape leave)
|
||||
(@f1 (enter Help))
|
||||
(@f2 (enter Rename))
|
||||
(@f3 (project new))
|
||||
(@f4 (enter History))
|
||||
(@f6 (enter Save))
|
||||
(@f8 (enter Options))
|
||||
(@f9 (enter Load))
|
||||
(@f10 (enter Quit))
|
||||
(@f12 (enter Devtools)))
|
||||
|
||||
(keys Back (@escape cancel))
|
||||
(keys Confirm (@enter confirm))
|
||||
(keys Page (@pgup page/up) (@pgdn page/down))
|
||||
(keys Delete (@delete delete) (@backspace delete/back))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
mod deps; pub use self::deps::*;
|
||||
|
||||
mod clock; pub use self::clock::*;
|
||||
|
||||
mod midi_octave; pub use self::midi_octave::*;
|
||||
mod midi_piano; pub use self::midi_piano::*;
|
||||
mod midi_point; pub use self::midi_point::*;
|
||||
|
|
@ -20,10 +22,6 @@ mod midi_play; pub use self::midi_play::*;
|
|||
mod midi_rec; pub use self::midi_rec::*;
|
||||
mod midi_seq; pub use self::midi_seq::*;
|
||||
|
||||
mod time_base; pub use self::time_base::*;
|
||||
mod time_moment; pub use self::time_moment::*;
|
||||
mod time_ticker; pub use self::time_ticker::*;
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
|
|
@ -33,7 +31,6 @@ macro_rules! def_sizes_iter {
|
|||
}
|
||||
|
||||
mod proj; pub use self::proj::*;
|
||||
mod proj_clock; pub use self::proj_clock::*;
|
||||
mod proj_device; pub use self::proj_device::*;
|
||||
mod proj_mixer; pub use self::proj_mixer::*;
|
||||
mod proj_port; pub use self::proj_port::*;
|
||||
|
|
|
|||
178
src/tek_ctrl.rs
178
src/tek_ctrl.rs
|
|
@ -23,13 +23,23 @@ impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x });
|
|||
}
|
||||
/// Enter a contextual mode.
|
||||
#[command(Enter = "enter")] pub fn cmd_mode_enter (&mut self, mode: Arc<str>) -> Perhaps<AppCmd> {
|
||||
self.modes_mut().push(mode);
|
||||
Ok(Some(AppCmd::Leave))
|
||||
let mut modes = self.modes_mut();
|
||||
Ok(if modes.last() != Some(&mode) {
|
||||
modes.push(mode);
|
||||
Some(AppCmd::Leave)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
/// Leave a contextual mode.
|
||||
#[command(Leave = "leave")] fn cmd_mode_leave (&mut self) -> Perhaps<AppCmd> {
|
||||
let mode = self.modes_mut().pop();
|
||||
let mut modes = self.modes_mut();
|
||||
if modes.len() > 1 {
|
||||
let mode = modes.pop();
|
||||
Ok(mode.map(|mode|AppCmd::Enter { mode }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
/// Navigate along a contextual axis.
|
||||
#[command(Axis = "axis")] pub fn cmd_axis (&mut self, command: AxisCmd) -> Perhaps<AppCmd> {
|
||||
|
|
@ -607,6 +617,87 @@ impl<T: HasClip + HasEditor
|
|||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||
}
|
||||
|
||||
impl<T> MidiClipControl for T where T: HasClip
|
||||
+ Namespace<Option<bool>> + Namespace<Option<ItemTheme>> {}
|
||||
#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClip
|
||||
+ Namespace<Option<bool>> + Namespace<Option<ItemTheme>>
|
||||
{
|
||||
/// Create a clip at the current position if it does not already exist.
|
||||
#[command(Upsert = "upsert")] fn clip_upsert (
|
||||
&mut self
|
||||
) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip_mut().map(|mut clip|if clip.is_none() {
|
||||
*clip = Some(Default::default());
|
||||
None // TODO undo
|
||||
} else {
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetColor = "color")] fn clip_set_color (
|
||||
&mut self, color: Option<ItemTheme>
|
||||
) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip_slot().map_inner_mut(|clip|MidiClipCmd::SetColor {
|
||||
color: Some(clip.set_color(color))
|
||||
}))
|
||||
}
|
||||
/// Toggle looping for the active clip
|
||||
#[command(SetLoop = "loop")] fn clip_toggle_loop (
|
||||
&mut self, looping: Option<bool>
|
||||
) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip_slot().map_inner_mut(|clip|MidiClipCmd::SetLoop {
|
||||
looping: Some(clip.toggle_loop(looping))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BrowseControl for T where T:
|
||||
Namespace<usize> + Namespace<PathBuf> + Namespace<Arc<str>> {}
|
||||
#[tek_proc::commands(BrowseCmd = "browse")] pub trait BrowseControl:
|
||||
Namespace<usize> + Namespace<PathBuf> + Namespace<Arc<str>>
|
||||
{
|
||||
/// Toggle visibility of browser
|
||||
#[command(Show = "show")] fn show (&mut self) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current directory
|
||||
#[command(Chdir = "chdir")] fn chdir (&mut self, _path: PathBuf) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current filter
|
||||
#[command(SetSearch = "set-search")] fn set_search (&mut self, _filter: Arc<str>) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set selected item
|
||||
#[command(SetCursor = "set-cursor")] fn set_cursor (&mut self, _index: usize) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(AxisCmd)] impl App {
|
||||
/// Increment the given data axis.
|
||||
#[command(Inc = "inc")] pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCmd::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
/// Decrement the given data axis.
|
||||
#[command(Dec = "dec")] pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCmd::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//impl<T: HasPool
|
||||
//+ Namespace<bool>
|
||||
//+ Namespace<usize>
|
||||
|
|
@ -822,84 +913,3 @@ impl<T: HasClip + HasEditor
|
|||
////})
|
||||
////}
|
||||
//}
|
||||
|
||||
impl<T> MidiClipControl for T where T: HasClip
|
||||
+ Namespace<Option<bool>> + Namespace<Option<ItemTheme>> {}
|
||||
#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClip
|
||||
+ Namespace<Option<bool>> + Namespace<Option<ItemTheme>>
|
||||
{
|
||||
/// Create a clip at the current position if it does not already exist.
|
||||
#[command(Upsert = "upsert")] fn clip_upsert (
|
||||
&mut self
|
||||
) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip_mut().map(|mut clip|if clip.is_none() {
|
||||
*clip = Some(Default::default());
|
||||
None // TODO undo
|
||||
} else {
|
||||
None
|
||||
}).flatten())
|
||||
}
|
||||
#[command(SetColor = "color")] fn clip_set_color (
|
||||
&mut self, color: Option<ItemTheme>
|
||||
) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip_slot().map_inner_mut(|clip|MidiClipCmd::SetColor {
|
||||
color: Some(clip.set_color(color))
|
||||
}))
|
||||
}
|
||||
/// Toggle looping for the active clip
|
||||
#[command(SetLoop = "loop")] fn clip_toggle_loop (
|
||||
&mut self, looping: Option<bool>
|
||||
) -> Perhaps<MidiClipCmd> {
|
||||
Ok(self.clip_slot().map_inner_mut(|clip|MidiClipCmd::SetLoop {
|
||||
looping: Some(clip.toggle_loop(looping))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BrowseControl for T where T:
|
||||
Namespace<usize> + Namespace<PathBuf> + Namespace<Arc<str>> {}
|
||||
#[tek_proc::commands(BrowseCmd = "browse")] pub trait BrowseControl:
|
||||
Namespace<usize> + Namespace<PathBuf> + Namespace<Arc<str>>
|
||||
{
|
||||
/// Toggle visibility of browser
|
||||
#[command(Show = "show")] fn show (&mut self) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current directory
|
||||
#[command(Chdir = "chdir")] fn chdir (&mut self, _path: PathBuf) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set current filter
|
||||
#[command(SetSearch = "set-search")] fn set_search (&mut self, _filter: Arc<str>) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
/// Set selected item
|
||||
#[command(SetCursor = "set-cursor")] fn set_cursor (&mut self, _index: usize) -> Perhaps<BrowseCmd> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(AxisCmd)] impl App {
|
||||
/// Increment the given data axis.
|
||||
#[command(Inc = "inc")] pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCmd::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
/// Decrement the given data axis.
|
||||
#[command(Dec = "dec")] pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
AppCmd::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ use crate::*;
|
|||
/// assert_eq!(modes.len(), 1);
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Modes(pub Arc<RwLock<BTreeMap<
|
||||
Arc<str>,
|
||||
Arc<Mode>
|
||||
>>>);
|
||||
pub struct Modes(pub Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>);
|
||||
|
||||
impl Modes {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)] pub struct Pool<T>(pub Arc<RwLock<Vec<Slot<T>>>>);
|
||||
|
||||
pub type SamplePool = Pool<Sample>;
|
||||
|
||||
pub type ClipPool = Pool<MidiClip>;
|
||||
impl<T> Clone for Pool<T> { fn clone (&self) -> Self { Pool(self.0.clone()) } }
|
||||
|
||||
impl<T> Clone for Pool<T> {
|
||||
fn clone (&self) -> Self { Pool(self.0.clone()) }
|
||||
}
|
||||
|
||||
impl<T> Pool<T> {
|
||||
pub fn slot (&self, index: usize) -> Slot<T> {
|
||||
let mut state = self.0.write();
|
||||
|
|
@ -67,7 +73,9 @@ impl<T> From<Vec<Option<T>>> for Pool<T> {
|
|||
}
|
||||
|
||||
#[derive(Default, Debug)] pub struct Slot<T>(pub Arc<RwLock<Option<Arc<RwLock<T>>>>>);
|
||||
|
||||
pub type SampleSlot = Slot<Sample>;
|
||||
|
||||
pub type ClipSlot = Slot<MidiClip>;
|
||||
|
||||
impl<T> Clone for Slot<T> {
|
||||
|
|
@ -95,10 +103,12 @@ impl<T> Slot<T> {
|
|||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SlotExt<T> {
|
||||
fn map_inner <U> (&self, cb: impl FnOnce(&T)->U) -> Option<U>;
|
||||
fn map_inner_mut <U> (&self, cb: impl FnOnce(&mut T)->U) -> Option<U>;
|
||||
}
|
||||
|
||||
impl<T> SlotExt<T> for Slot<T> {
|
||||
fn map_inner <U> (&self, cb: impl FnOnce(&T)->U) -> Option<U> {
|
||||
self.read().as_ref().map(|x|cb(&*x.read()))
|
||||
|
|
@ -107,6 +117,7 @@ impl<T> SlotExt<T> for Slot<T> {
|
|||
self.read().as_ref().map(|x|cb(&mut*x.write()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SlotExt<T> for Option<Slot<T>> {
|
||||
fn map_inner <U> (&self, cb: impl FnOnce(&T)->U) -> Option<U> {
|
||||
self.as_ref().map(|x|x.map_inner(cb))?
|
||||
|
|
|
|||
113
src/tek_view.rs
113
src/tek_view.rs
|
|
@ -275,13 +275,15 @@ impl View<Tui, App> {
|
|||
}
|
||||
},
|
||||
|
||||
"exact" | "min" | "max" | "push" | "pull" | "pad" => {
|
||||
"exact" | "min" | "max" | "push" | "pull" | "pad" | "shrink" | "expand" => {
|
||||
match head.split('/').skip(1).next() {
|
||||
Some("w") | Some("x") => {
|
||||
let value = Arc::from(arg!(expr, head, 1, "value"));
|
||||
let value = move|state: &App|state.namespace(&value);
|
||||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||||
match ns {
|
||||
"shrink" => Self::boxed(move|state, screen|Shrink::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"expand" => Self::boxed(move|state, screen|Expand::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
|
|
@ -296,6 +298,8 @@ impl View<Tui, App> {
|
|||
let value = move|state: &App|state.namespace(&value);
|
||||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||||
match ns {
|
||||
"shrink" => Self::boxed(move|state, screen|Shrink::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"expand" => Self::boxed(move|state, screen|Expand::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
|
|
@ -312,6 +316,8 @@ impl View<Tui, App> {
|
|||
let value2 = move|state: &App|state.namespace(&value2);
|
||||
let thunk = Self::compile(arg!(expr, head, 3, "content"))?;
|
||||
match ns {
|
||||
"shrink" => Self::boxed(move|state, screen|Shrink::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"expand" => Self::boxed(move|state, screen|Expand::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
|
|
@ -385,7 +391,8 @@ impl View<Tui, App> {
|
|||
Some("Editor") => draw!("TODO editor"),
|
||||
Some("MetersIn") => draw!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())),
|
||||
Some("MetersOut") => draw!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())),
|
||||
Some("ModeList") => draw!(|app|app.modes.read().get(0)), // TODO all
|
||||
Some("ModeList") => draw!(|app|view_modes(app)),
|
||||
Some("HistoryCount") => draw!(|app|app.history.len()),
|
||||
Some("ScenesClips") => draw!(|app|view_scenes_clips(app)),
|
||||
Some("ScenesNames") => draw!(|app|app.view_scenes_names()),
|
||||
Some("Sessions") => draw!(|___|view_sessions()),
|
||||
|
|
@ -922,6 +929,26 @@ impl ScenesView for Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw<Tui> + 'a {
|
||||
draw(move|to: &mut Tui|{
|
||||
let XYWH(x, y, w, h) = to.area();
|
||||
let mut x_next = x;
|
||||
let mut w_used = 0u16;
|
||||
for item in state.modes().iter() {
|
||||
let area = XYWH(x_next, y, w.minus(w_used), h);
|
||||
if let Some(used) = to.draw(area, item)? {
|
||||
let used_w = used.w() + 1;
|
||||
x_next = x_next.plus(used_w);
|
||||
w_used = w_used.plus(used_w);
|
||||
if w_used >= w {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(XYWH(x, y, w_used, h)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw clips per scene
|
||||
fn view_scenes_clips <S: TracksView + ScenesView> (state: &S) -> impl Draw<Tui> {
|
||||
let select = state.selection();
|
||||
|
|
@ -1536,19 +1563,81 @@ impl<T: PartialEq, U> Memo<T, U> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
/// Contains memoized renders of clock values.
|
||||
///
|
||||
/// Performance optimization.
|
||||
#[derive(Debug)] pub struct ClockView {
|
||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||
pub buf: Memo<Option<f64>, String>,
|
||||
pub lat: Memo<Option<f64>, String>,
|
||||
pub bpm: Memo<Option<f64>, String>,
|
||||
pub beat: Memo<Option<f64>, String>,
|
||||
pub time: Memo<Option<f64>, String>,
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
impl_default!(ClockView: {
|
||||
let mut beat = String::with_capacity(16);
|
||||
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
|
||||
let mut time = String::with_capacity(16);
|
||||
let _ = write!(time, "{}", Self::TIME_EMPTY);
|
||||
let mut bpm = String::with_capacity(16);
|
||||
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
|
||||
Self {
|
||||
beat: Memo::new(None, beat),
|
||||
time: Memo::new(None, time),
|
||||
bpm: Memo::new(None, bpm),
|
||||
sr: Memo::new(None, String::with_capacity(16)),
|
||||
buf: Memo::new(None, String::with_capacity(16)),
|
||||
lat: Memo::new(None, String::with_capacity(16)),
|
||||
}
|
||||
});
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
impl ClockView {
|
||||
pub const BEAT_EMPTY: &'static str = "-.-.--";
|
||||
pub const TIME_EMPTY: &'static str = "-.---s";
|
||||
pub const BPM_EMPTY: &'static str = "---.---";
|
||||
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
|
||||
let rate = clock.timebase.sr.get();
|
||||
let chunk = clock.chunk.load(Relaxed) as f64;
|
||||
let lat = chunk / rate * 1000.;
|
||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||
let mut cache = cache.try_write().unwrap();
|
||||
|
||||
cache.buf.update(
|
||||
Some(chunk), rewrite!(buf, "{chunk}")
|
||||
);
|
||||
|
||||
cache.lat.update(
|
||||
Some(lat), rewrite!(buf, "{lat:.1}ms")
|
||||
);
|
||||
|
||||
cache.sr.update(
|
||||
Some((compact, rate)), |buf: &mut String,_,_|{
|
||||
buf.clear();
|
||||
if compact {
|
||||
write!(buf, "{:.1}kHz", rate / 1000.)
|
||||
} else {
|
||||
write!(buf, "{:.0}Hz", rate)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
|
||||
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||
let time = now/1000000.;
|
||||
let bpm = clock.timebase.bpm.get();
|
||||
cache.beat.update(Some(pulse), |buf, _, _|{
|
||||
buf.clear();
|
||||
clock.timebase.format_beats_1_to(buf, pulse)
|
||||
});
|
||||
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
|
||||
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
|
||||
} else {
|
||||
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
|
||||
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
|
||||
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_view {
|
||||
|
|
|
|||
159
src/time_base.rs
159
src/time_base.rs
|
|
@ -1,159 +0,0 @@
|
|||
use crate::*;
|
||||
use ::std::sync::Arc;
|
||||
use ::atomic_float::AtomicF64;
|
||||
|
||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Timebase::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct Timebase {
|
||||
/// Audio samples per second
|
||||
pub sr: SampleRate,
|
||||
/// MIDI beats per minute
|
||||
pub bpm: Bpm,
|
||||
/// MIDI ticks per beat
|
||||
pub ppq: Ppq,
|
||||
}
|
||||
|
||||
/// Audio sample rate in Hz (samples per second)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
||||
|
||||
/// Tempo in beats per minute
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64);
|
||||
|
||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64);
|
||||
|
||||
impl Timebase {
|
||||
/// Specify sample rate, BPM and PPQ
|
||||
pub fn new (
|
||||
s: impl Into<SampleRate>,
|
||||
b: impl Into<Bpm>,
|
||||
p: impl Into<Ppq>
|
||||
) -> Self {
|
||||
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
|
||||
}
|
||||
/// Iterate over ticks between start and end.
|
||||
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker {
|
||||
Ticker { spp: self.samples_per_pulse(), sample: start, start, end }
|
||||
}
|
||||
/// Return the duration fo a beat in microseconds
|
||||
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
|
||||
/// Return the number of beats in a second
|
||||
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
|
||||
/// Return the number of microseconds corresponding to a note of the given duration
|
||||
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
|
||||
4.0 * self.usec_per_beat() * num / den
|
||||
}
|
||||
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
|
||||
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
|
||||
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
|
||||
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
|
||||
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
|
||||
/// Return number of pulses in a second (BPM-dependent)
|
||||
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
|
||||
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_per_sample (&self) -> f64 {
|
||||
self.usec_per_pulse() / self.sr.usec_per_sample()
|
||||
}
|
||||
/// Return number of samples in a pulse (SR- and BPM-dependent)
|
||||
#[inline] pub fn samples_per_pulse (&self) -> f64 {
|
||||
self.sr.get() / self.pulses_per_second()
|
||||
}
|
||||
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
|
||||
self.pulses_per_sample() * p
|
||||
}
|
||||
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
|
||||
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
|
||||
s / self.pulses_per_sample()
|
||||
}
|
||||
/// Return the number of samples corresponding to a note of the given duration
|
||||
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
|
||||
self.usec_to_sample(self.note_to_usec(note))
|
||||
}
|
||||
/// Return the number of samples corresponding to the given number of microseconds
|
||||
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
|
||||
usec * self.sr.get() / 1000f64
|
||||
}
|
||||
/// Return the quantized position of a moment in time given a step
|
||||
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
|
||||
let step = self.note_to_usec(step);
|
||||
(time / step, time % step)
|
||||
}
|
||||
/// Quantize a collection of events
|
||||
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
|
||||
&self, step: (f64, f64), events: E
|
||||
) -> Vec<(f64, f64)> {
|
||||
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 0
|
||||
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc<str> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar starting from 0
|
||||
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||
format!("{}.{}", beats / 4, beats % 4).into()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
|
||||
let mut string = String::with_capacity(16);
|
||||
self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat");
|
||||
string.into()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1)
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||
format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleRate {
|
||||
/// Return the duration of a sample in microseconds (floating)
|
||||
#[inline] pub fn usec_per_sample (&self) -> f64 {
|
||||
1_000_000f64 / self.get()
|
||||
}
|
||||
/// Return the duration of a sample in microseconds (floating)
|
||||
#[inline] pub fn sample_per_usec (&self) -> f64 {
|
||||
self.get() / 1_000_000f64
|
||||
}
|
||||
/// Convert a number of samples to microseconds (floating)
|
||||
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
|
||||
self.usec_per_sample() * samples
|
||||
}
|
||||
/// Convert a number of microseconds to samples (floating)
|
||||
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
|
||||
self.sample_per_usec() * usecs
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
use crate::*;
|
||||
use ::std::sync::Arc;
|
||||
use ::atomic_float::AtomicF64;
|
||||
|
||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Moment::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)] pub struct Moment {
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current time in microseconds
|
||||
pub usec: Microsecond,
|
||||
/// Current time in audio samples
|
||||
pub sample: SampleCount,
|
||||
/// Current time in MIDI pulses
|
||||
pub pulse: Pulse,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::Moment2::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
||||
#[default] None,
|
||||
Zero,
|
||||
Usec(Microsecond),
|
||||
Sample(SampleCount),
|
||||
Pulse(Pulse),
|
||||
}
|
||||
|
||||
/// Timestamp in microseconds
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in audio samples
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in MIDI pulses
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
||||
|
||||
impl Moment {
|
||||
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
||||
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
||||
}
|
||||
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
|
||||
Self {
|
||||
usec: usec.into(),
|
||||
sample: timebase.sr.usecs_to_sample(usec).into(),
|
||||
pulse: timebase.usecs_to_pulse(usec).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
|
||||
Self {
|
||||
sample: sample.into(),
|
||||
usec: timebase.sr.samples_to_usec(sample).into(),
|
||||
pulse: timebase.samples_to_pulse(sample).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
|
||||
Self {
|
||||
pulse: pulse.into(),
|
||||
sample: timebase.pulses_to_sample(pulse).into(),
|
||||
usec: timebase.pulses_to_usec(pulse).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
#[inline] pub fn update_from_usec (&self, usec: f64) {
|
||||
self.usec.set(usec);
|
||||
self.pulse.set(self.timebase.usecs_to_pulse(usec));
|
||||
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
|
||||
}
|
||||
#[inline] pub fn update_from_sample (&self, sample: f64) {
|
||||
self.usec.set(self.timebase.sr.samples_to_usec(sample));
|
||||
self.pulse.set(self.timebase.samples_to_pulse(sample));
|
||||
self.sample.set(sample);
|
||||
}
|
||||
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
|
||||
self.usec.set(self.timebase.pulses_to_usec(pulse));
|
||||
self.pulse.set(pulse);
|
||||
self.sample.set(self.timebase.pulses_to_sample(pulse));
|
||||
}
|
||||
#[inline] pub fn format_beat (&self) -> Arc<str> {
|
||||
self.timebase.format_beats_1(self.pulse.get()).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Microsecond {
|
||||
#[inline] pub fn format_msu (&self) -> Arc<str> {
|
||||
let usecs = self.get() as usize;
|
||||
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||
format!("{minutes}:{seconds:02}:{msecs:03}").into()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
///
|
||||
/// ```
|
||||
/// let iter = tek::Ticker::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Ticker {
|
||||
pub spp: f64,
|
||||
pub sample: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Iterator for Ticker {
|
||||
type Item = (usize, usize);
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.sample > self.end { return None }
|
||||
let spp = self.spp;
|
||||
let sample = self.sample as f64;
|
||||
let start = self.start;
|
||||
let end = self.end;
|
||||
self.sample += 1;
|
||||
//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 / spp) as usize;
|
||||
return Some((time, tick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
tengri
2
tengri
|
|
@ -1 +1 @@
|
|||
Subproject commit 21d6674ffb81b78eb7efe281c43f0a085cbd94ad
|
||||
Subproject commit 1e8b18134d8ad234e5d0142d706f21f1366668d0
|
||||
Loading…
Add table
Add a link
Reference in a new issue