diff --git a/src/.scratch.rs b/src/.scratch.rs index 30650f90..31920178 100644 --- a/src/.scratch.rs +++ b/src/.scratch.rs @@ -1614,3 +1614,60 @@ //})?; //Ok(bind) //} + +//impl App { + ///// Toggle MIDI editor. + ///// + ///// ``` + ///// tek::App::default().toggle_editor(None); + ///// ``` + //pub fn toggle_editor (&mut self, value: Option) { + ////FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + //if value.unwrap_or_else(||!self.editor().is_some()) { + //self.toggle_editor_on() + //} else { + //self.toggle_editor_off() + //} + //} + + //fn toggle_editor_on (&mut self) { + //// Create new clip in pool when entering empty cell + //if let Selection::TrackClip { track: track_index, scene } = *self.selection() + //&& let Some(scene) = self.project.scenes.get_mut(scene) + //&& scene.clips.get(track_index).is_none() + //&& let Some(track) = self.project.tracks.get_mut(track_index) + //{ + //let color = track.color.base.mix(scene.color.base, 0.5); + //let color = ItemColor::random_near(color, 0.2).into(); + //let clip: MidiClip = MidiClip { color, ..Default::default() }; + //let clip = self.project.clips.append(Some(clip)); + //// autocolor: new clip colors from scene and track color + //if let Some(editor) = &mut self.project.editor { + //*editor.mode.clip.write() = clip.0.read().clone(); + //editor.mode.color = clip.0.read().as_ref().map(|p|p.read().color) + //.unwrap_or(ItemTheme::G[64]); + //} + //*scene.clips.slot(track_index).write() = clip.0.read().clone(); + ////Some(clip) + //} else { + ////None + //} + //} + + //fn toggle_editor_off (&mut self) { + //if let Selection::TrackClip { track, scene } = *self.selection() + //&& let Some(scene) = self.project.scenes.get_mut(scene) + //{ + //// Remove clip from arrangement when exiting empty clip editor + //let mut swapped = None; + //if let Some(count) = scene.clips.slot(track).read().as_ref().map(|c|c.read().count_midi_messages()) + //&& count == 0 + //{ + //std::mem::swap(&mut swapped, &mut scene.clips.slot(track).write()); + //} + //if let Some(clip) = swapped { + //self.project.clips.remove(&*clip.read()); + //} + //} + //} +//} diff --git a/src/conf/view.rs b/src/conf/view.rs index 3b309509..0ba487b6 100644 --- a/src/conf/view.rs +++ b/src/conf/view.rs @@ -1434,3 +1434,45 @@ pub fn print_config_views (config: &Config) { println!("{} {} {}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")), v.source); }); } + +impl Project { + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } +} + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +pub trait HasClipsSize { + /// Size of clips area. + fn clips_size (&self) -> &Sizer; + /// Draw content annotated with value of [Self::clips_size]. + fn with_clips_size (&self, show: bool, content: impl Draw) -> impl Draw { + let size = self.clips_size(); + above( + when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()), + size.of(content.align_c().full_wh()) + ) + } +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Sizer { + &self.project.size_inner + } +} diff --git a/src/gear/device.rs b/src/gear/device.rs index 50fe8020..18cd9d9b 100644 --- a/src/gear/device.rs +++ b/src/gear/device.rs @@ -41,6 +41,16 @@ impl App { } } +/// Has a chain of devices. +pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { + self.as_ref() + } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() + } +} + #[tek_proc::command(Device)] #[tek_proc::keyword(Device)] #[derive(Debug)] diff --git a/src/gear/midi.rs b/src/gear/midi.rs index 9111b8a7..1fccdd22 100644 --- a/src/gear/midi.rs +++ b/src/gear/midi.rs @@ -2,623 +2,6 @@ use crate::*; use ::std::sync::Arc; use ::atomic_float::AtomicF64; -impl App { - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock( - &self.project.clock.view_cache, self.clock(), self.size.w() > 80 - ) - } -} - -/// The source of time. -/// -/// ``` -/// let clock = tek::Clock::default(); -/// ``` -#[derive(Clone, Default)] pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -//impl Act for ClockCmd { - //fn act (&self, state: &mut T) -> Perhaps { - //self.act(state.clock_mut()) // awesome - //} -//} - -/// Quantization setting for launching clips -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); - -/// Quantization setting for notes -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit {} - -pub const DEFAULT_PPQ: f64 = 96.0; - -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; - -/// (pulses, name), assuming 96 PPQ -pub const NOTE_DURATIONS: [(usize, &str);26] = [ - (1, "1/384"), (2, "1/192"), - (3, "1/128"), (4, "1/96"), - (6, "1/64"), (8, "1/48"), - (12, "1/32"), (16, "1/24"), - (24, "1/16"), (32, "1/12"), - (48, "1/8"), (64, "1/6"), - (96, "1/4"), (128, "1/3"), - (192, "1/2"), (256, "2/3"), - (384, "1/1"), (512, "4/3"), - (576, "3/2"), (768, "2/1"), - (1152, "3/1"), (1536, "4/1"), - (2304, "6/1"), (3072, "8/1"), - (3456, "9/1"), (6144, "16/1"), -]; - -pub const NOTE_NAMES: [&str; 128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", -]; - -impl LaunchSync { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } -} - -impl Quantize { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } -} - -/// Implement an arithmetic operation for a unit of time -#[macro_export] macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} - -impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } -} - -impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &Bpm { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &Ppq { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.try_read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.try_read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.try_write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.try_read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } -} - -/// 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, - b: impl Into, - p: impl Into - ) -> 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 + 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 { - 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 { - 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 { - 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 { - 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, - /// 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) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, 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, 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, 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 { - self.timebase.format_beats_1(self.pulse.get()).into() - } -} - -impl Microsecond { - #[inline] pub fn format_msu (&self) -> Arc { - 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 { - 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)) - } - } - } -} - /// Contains state for playing a clip /// /// ``` @@ -1752,59 +1135,72 @@ impl std::fmt::Debug for PianoHorizontal { } } -//impl App { - ///// Toggle MIDI editor. - ///// - ///// ``` - ///// tek::App::default().toggle_editor(None); - ///// ``` - //pub fn toggle_editor (&mut self, value: Option) { - ////FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - //if value.unwrap_or_else(||!self.editor().is_some()) { - //self.toggle_editor_on() - //} else { - //self.toggle_editor_off() - //} - //} +/// Has a MIDI editor. +/// +/// ``` +/// use tek::{*, tengri::*}; +/// +/// struct Test(Option); +/// impl_as_ref!(Option: |self: Test|&self.0); +/// impl_as_mut!(Option: |self: Test|&mut self.0); +/// +/// let mut host = Test(Some(MidiEditor::default())); +/// let _ = host.editor(); +/// let _ = host.editor_mut(); +/// let _ = host.is_editing(); +/// let _ = host.editor_w(); +/// let _ = host.editor_h(); +/// ``` +impl HasEditor for T where T: + AsRef> + AsMut> {} +pub trait HasEditor: + AsRef> + AsMut> +{ + fn editor (&self) -> &Option { + self.as_ref() + } + fn editor_mut (&mut self) -> &mut Option { + self.as_mut() + } +} - //fn toggle_editor_on (&mut self) { - //// Create new clip in pool when entering empty cell - //if let Selection::TrackClip { track: track_index, scene } = *self.selection() - //&& let Some(scene) = self.project.scenes.get_mut(scene) - //&& scene.clips.get(track_index).is_none() - //&& let Some(track) = self.project.tracks.get_mut(track_index) - //{ - //let color = track.color.base.mix(scene.color.base, 0.5); - //let color = ItemColor::random_near(color, 0.2).into(); - //let clip: MidiClip = MidiClip { color, ..Default::default() }; - //let clip = self.project.clips.append(Some(clip)); - //// autocolor: new clip colors from scene and track color - //if let Some(editor) = &mut self.project.editor { - //*editor.mode.clip.write() = clip.0.read().clone(); - //editor.mode.color = clip.0.read().as_ref().map(|p|p.read().color) - //.unwrap_or(ItemTheme::G[64]); - //} - //*scene.clips.slot(track_index).write() = clip.0.read().clone(); - ////Some(clip) - //} else { - ////None - //} - //} +/// Has a pair of buffers for MIDI IO. +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} - //fn toggle_editor_off (&mut self) { - //if let Selection::TrackClip { track, scene } = *self.selection() - //&& let Some(scene) = self.project.scenes.get_mut(scene) - //{ - //// Remove clip from arrangement when exiting empty clip editor - //let mut swapped = None; - //if let Some(count) = scene.clips.slot(track).read().as_ref().map(|c|c.read().count_midi_messages()) - //&& count == 0 - //{ - //std::mem::swap(&mut swapped, &mut scene.clips.slot(track).write()); - //} - //if let Some(clip) = swapped { - //self.project.clips.remove(&*clip.read()); - //} - //} - //} -//} +/// Can have a currently playing clip. +pub trait HasPlayClip: HasClock { + fn reset (&self) -> bool; + fn reset_mut (&mut self) -> &mut bool; + fn play_clip (&self) -> &Option<(Moment, Option>>)>; + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn next_clip (&self) -> &Option<(Moment, Option>>)>; + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; +} + +/// Has a MIDI sequencer. +impl HasSequencer for T where T: AsRef + AsMut {} +pub trait HasSequencer: AsRef + AsMut { + fn sequencer (&self) -> &Sequencer { + self.as_ref() + } + fn sequencer_mut (&mut self) -> &mut Sequencer { + self.as_mut() + } +} +pub trait HasClip { + fn clip_slot (&self) -> Option>; + fn clip (&self) -> Option>>>> { + self.clip_slot().map(|slot|slot.read()) + } + fn clip_mut (&self) -> Option>>>> { + self.clip_slot().map(|slot|slot.write()) + } +} +impl HasClip for App { + fn clip_slot (&self) -> Option> { + self.project.clip_slot() + } +} diff --git a/src/gear/pool.rs b/src/proj/pool.rs similarity index 100% rename from src/gear/pool.rs rename to src/proj/pool.rs diff --git a/src/proj/scene.rs b/src/proj/scene.rs new file mode 100644 index 00000000..030f1774 --- /dev/null +++ b/src/proj/scene.rs @@ -0,0 +1,111 @@ +use crate::*; + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Pool, +} + +impl Scene { + pub const DEFAULT_HEIGHT: usize = 2; + /// Get pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.read().iter().fold(0, |a, p|{ + a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0)) + }) + } + /// True if all clips in scene are currently playing on given tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + // Not "playing" if there are no clips + if !self.clips.read().iter().any(|clip|clip.read().is_some()) { + return false + } + // Playing if at least 1 clip is playing + self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read() == *c.read() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } +} + +pub trait HasScenes { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scenes_scroll (&self) -> usize { + 0 + } +} +impl HasScenes for Project { + fn scenes_scroll (&self) -> usize { + self.scenes_scroll + } + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } +} +impl HasScenes for App { + fn scenes_scroll (&self) -> usize { + self.project.scenes_scroll() + } + fn scenes (&self) -> &Vec { + self.project.scenes() + } + fn scenes_mut (&mut self) -> &mut Vec { + self.project.scenes_mut() + } +} + +pub trait HasScene: AsRefOpt + AsMutOpt { + fn scene (&self) -> Option<&Scene> { + self.as_ref_opt() + } + fn scene_mut (&mut self) -> Option<&mut Scene> { + self.as_mut_opt() + } +} +impl HasScene for T where T: AsRefOpt + AsMutOpt + Send + Sync {} + +impl ScenesView for App { + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(self.w_side()) + } + fn w_side (&self) -> u16 { + 20 + } + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } +} +impl ScenesView for Project { + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.size.w() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} diff --git a/src/proj/select.rs b/src/proj/select.rs new file mode 100644 index 00000000..649b4c58 --- /dev/null +++ b/src/proj/select.rs @@ -0,0 +1,183 @@ +use crate::*; + +impl HasSelection for T where T: AsRef + AsMut {} +pub trait HasSelection: AsRef + AsMut { + fn selection (&self) -> &Selection { + self.as_ref() + } + fn selection_mut (&mut self) -> &mut Selection { + self.as_mut() + } + /// Get the active track + #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() + } +} + +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] +pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A clip is selected. + Clip(usize), + /// A sample is selected. + Sample(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip slot (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} + +impl Selection { + + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match &s.clips.get(*track) { + None => format!("T{track} S{scene}: Empty"), + Some(clip) => format!("T{track} S{scene} C{}", + clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + + #[cfg(feature = "scene")] + pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + + #[cfg(feature = "scene")] + pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + + #[cfg(feature = "scene")] + pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + + #[cfg(feature = "scene")] + pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + + #[cfg(feature = "track")] + pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + + #[cfg(feature = "track")] + pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + + #[cfg(feature = "track")] + pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + + #[cfg(feature = "track")] + pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } + +} diff --git a/src/proj/time.rs b/src/proj/time.rs new file mode 100644 index 00000000..5b307fd4 --- /dev/null +++ b/src/proj/time.rs @@ -0,0 +1,670 @@ +use crate::*; + +impl_has!(Clock: |self: App| self.project.clock); +impl_has!(Clock: |self: Project| self.clock); +impl_has!(Clock: |self: Track| self.sequencer.clock); + +impl HasClock for T where T: AsRef + AsMut {} + +/// Has a clock. +pub trait HasClock: AsRef + AsMut { + fn clock (&self) -> &Clock { + self.as_ref() + } + fn clock_mut (&mut self) -> &mut Clock { + self.as_mut() + } + /// Update memoized render of clock values. + /// ``` + /// # use tek::*; + /// App::default().clock_update(); + /// ``` + fn clock_update (&self) where Self: AsRef { + let clock = self.clock(); + let sizer: &Sizer = self.as_ref(); + ClockView::update_clock(&clock.view_cache, clock, sizer.w() > 80) + } +} + +/// The source of time. +/// +/// ``` +/// let clock = tek::Clock::default(); +/// ``` +#[derive(Clone, Default)] pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +//impl Act for ClockCmd { + //fn act (&self, state: &mut T) -> Perhaps { + //self.act(state.clock_mut()) // awesome + //} +//} + +/// Quantization setting for launching clips +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64); + +/// Quantization setting for notes +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64); + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit {} + +pub const DEFAULT_PPQ: f64 = 96.0; + +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// (pulses, name), assuming 96 PPQ +pub const NOTE_DURATIONS: [(usize, &str);26] = [ + (1, "1/384"), (2, "1/192"), + (3, "1/128"), (4, "1/96"), + (6, "1/64"), (8, "1/48"), + (12, "1/32"), (16, "1/24"), + (24, "1/16"), (32, "1/12"), + (48, "1/8"), (64, "1/6"), + (96, "1/4"), (128, "1/3"), + (192, "1/2"), (256, "2/3"), + (384, "1/1"), (512, "4/3"), + (576, "3/2"), (768, "2/1"), + (1152, "3/1"), (1536, "4/1"), + (2304, "6/1"), (3072, "8/1"), + (3456, "9/1"), (6144, "16/1"), +]; + +pub const NOTE_NAMES: [&str; 128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; + +impl LaunchSync { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} + +impl Quantize { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} + +/// Implement an arithmetic operation for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + +impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } +} + +impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &Bpm { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &Ppq { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.try_read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.try_read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.try_write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.try_read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } +} + +/// 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, + b: impl Into, + p: impl Into + ) -> 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 + 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 { + 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 { + 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 { + 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 { + 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, + /// 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) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, 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, 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, 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 { + self.timebase.format_beats_1(self.pulse.get()).into() + } +} + +impl Microsecond { + #[inline] pub fn format_msu (&self) -> Arc { + 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 { + 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)) + } + } + } +} + +/// Define and implement a unit of time +macro_rules! impl_time_unit { + ($T:ident) => { + impl TimeUnit for $T {} + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl_from!($T: |value: f64| { Self(value.into()) }); + impl_from!($T: |value: usize| { Self((value as f64).into()) }); + impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } + impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } + impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } + impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } + impl $T { + pub fn get (&self) -> f64 { + self.0.load(Relaxed) + } + pub fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Relaxed); + old + } + } + } +} + +impl_time_unit!(SampleCount); +impl_time_unit!(SampleRate); +impl_time_unit!(Microsecond); +impl_time_unit!(Quantize); +impl_time_unit!(Ppq); +impl_time_unit!(Pulse); +impl_time_unit!(Bpm); +impl_time_unit!(LaunchSync); diff --git a/src/gear/track.rs b/src/proj/track.rs similarity index 58% rename from src/gear/track.rs rename to src/proj/track.rs index 0a206704..80398421 100644 --- a/src/gear/track.rs +++ b/src/proj/track.rs @@ -79,3 +79,69 @@ impl HasWidth for Track { if self.width > Track::MIN_WIDTH { self.width -= 1; } } } + +impl HasTrack for T where T: AsRefOpt + AsMutOpt {} + +pub trait HasTrack: AsRefOpt + AsMutOpt { + fn track (&self) -> Option<&Track> { + self.as_ref_opt() + } + fn track_mut (&mut self) -> Option<&mut Track> { + self.as_mut_opt() + } +} + +impl HasTracks for T where T: AsRef> + AsMut> + HasClock + HasTrackScroll {} + +pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrackScroll { + /// Read-only reference to collection of [Track]s. + fn tracks (&self) -> &Vec { + self.as_ref() + } + /// Mutable reference to collection of [Track]s. + fn tracks_mut (&mut self) -> &mut Vec { + self.as_mut() + } +} + +pub trait HasTrackScroll { + fn track_scroll (&self) -> usize; + fn tracks_last (&self) -> usize; + fn tracks_last_mut (&mut self) -> &mut usize; +} + +impl HasTrackScroll for &mut Project { + fn track_scroll (&self) -> usize { + self.track_scroll + } + fn tracks_last (&self) -> usize { + self.track_last + } + fn tracks_last_mut (&mut self) -> &mut usize { + &mut self.track_last + } +} + +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { + self.project.track_scroll() + } + fn tracks_last (&self) -> usize { + self.project.track_last + } + fn tracks_last_mut (&mut self) -> &mut usize { + &mut self.project.track_last + } +} + +impl HasTrackScroll for Project { + fn track_scroll (&self) -> usize { + self.track_scroll + } + fn tracks_last (&self) -> usize { + self.track_last + } + fn tracks_last_mut (&mut self) -> &mut usize { + &mut self.track_last + } +} diff --git a/src/tek.rs b/src/tek.rs index e3aa82bc..dae36cbc 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -8,9 +8,9 @@ pub use self::deps::*; mod deps; fn main () -> Usually<()> { tengri::Tui::setup_panic(); #[cfg(not(feature = "cli"))] - let outcome = Watch::run_watched(main_plain, None); + let outcome = config_watch_run(main_plain, None); #[cfg(feature = "cli")] - let outcome = Watch::run_watched(main_cli, None); + let outcome = config_watch_run(main_cli, None); if let Err(e) = outcome { println!("{e:#?}"); std::process::exit(1); @@ -54,7 +54,6 @@ fn main_plain (config: Arc) -> Usually<()> { /// # use tek::*; /// let mut app = App::default(); /// let _ = app.scenes_add_one(None, None).unwrap(); -/// let _ = app.update_clock(); /// app.project.editor = Some(Default::default()); /// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); /// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); @@ -62,14 +61,6 @@ fn main_plain (config: Arc) -> Usually<()> { /// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); /// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); /// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -/// let _ = app.project.w(); -/// //let _ = app.project.w_sidebar(); -/// //let _ = app.project.w_tracks_area(); -/// let _ = app.project.h(); -/// //let _ = app.project.h_tracks_area(); -/// //let _ = app.project.h_inputs(); -/// //let _ = app.project.h_outputs(); -/// let _ = app.project.h_scenes(); /// ``` #[derive(Default, Debug)] #[namespace(Arc<[Connect]> tek_arc_array_connect)] @@ -133,34 +124,22 @@ impl App { /// ``` /// # use ::tek::{*, tengri::Jack}; /// let proj = Project::new_jack(&"test_tek").expect("failed to connect to jack"); - /// let tek = config_reload(App::new(None, proj, None, "Arranger"), stringify!( - /// (mode Arranger (name Arranger) (info Launch grid.) - /// (axis X Track) - /// (axis Y Scene) - /// (bind Global Clock Tracks Scenes Clip Launch Color History) - /// (view (bar/n 2 StatusBar (bar/s 8 (bar/e 16 (align/nw Transport) - /// (bar/w 20 (bar/s 2 (bar/w 4 TracksOutputsAdd TracksOutputsCount) - /// (bar/s 2 (bar/w 4 TracksDevicesAdd TracksDevicesCount) - /// (bar/s 2 (bar/w 4 TracksNamesAdd TracksNamesCount) - /// (bar/w 4 TracksInputsAdd TracksInputsCount)))) - /// (align/x (bar/s 2 TracksOutputsCells - /// (bar/s 2 TracksDevicesCells - /// (bar/s 2 TracksNamesCells TracksInputsCells)))))) - /// (bar/w 20 (text) (bar/e 16 ScenesNames ScenesClips)))))) - /// )).unwrap(); + /// let conf = include_str!("tek.edn"); + /// let mode = vec!["Arranger".into(), "Menu".into()]; + /// let tek = config_reload(App::new(None, proj, None, mode), conf).unwrap(); /// ``` pub fn new ( exit: Option, project: Project, watch: impl Into>>, - mode: impl AsRef + modes: impl Into>>>, ) -> Self { App { exit: exit.unwrap_or_default(), jack: project.jack.clone(), color: ItemTheme::random(), dialog: Dialog::welcome(), - stack: Arc::new(RwLock::new(vec![mode.as_ref().into()])), + stack: Arc::new(RwLock::new(modes.into().unwrap_or_default())), watch: watch.into().unwrap_or_default(), project, ..Default::default() @@ -168,6 +147,157 @@ impl App { } } +pub use self::conf::*; mod conf { + use crate::*; + + mod axis; pub use self::axis::*; + mod bind; pub use self::bind::*; + mod lang; pub use self::lang::*; + mod mode; pub use self::mode::*; + mod view; pub use self::view::*; + + pub trait HasError { + fn error (&self) -> &RwLock>; + fn error_set (&self, e: Option) { + *self.error().write() = e; + } + } + impl> HasError> for T { + fn error (&self) -> &RwLock>> { + &self.as_ref().error + } + } + impl HasError> for Arc { + fn error (&self) -> &RwLock>> { + &self.as_ref().config.error + } + } + + pub trait HasConfig { + fn config (&self) -> &Config; + /// Print the configuration. + fn config_print (&self) { + let config = self.config(); + print_config_views(config); + print_config_binds(config); + print_config_modes(config); + } + /// Make this configuration empty. + fn config_clear (&self) { + let config = self.config(); + config.modes_clear(); + config.views_clear(); + config.binds_clear(); + config.stamp.store(quanta::Clock::new().raw(), Relaxed); + } + } + impl HasConfig for App { + fn config (&self) -> &Config { + &self.watch.config + } + } + impl HasConfig for Watch { + fn config (&self) -> &Config { + &self.config + } + } + impl HasConfig for Config { + fn config (&self) -> &Config { + self + } + } + + /// Default configuration contents. + const CONFIG_DEFAULT: &'static str = include_str!("tek.edn"); + + /// Create, initialize, and watch a new configuration. + pub fn config_watch_run ( + call: impl FnOnce(Arc)->T, + dirs: impl Into>, + ) -> Usually { + let config = Arc::new(Watch::new(dirs.into())); + config_init(config.as_ref())?; + let watcher = config.clone().watch_config(None)?; + *config.watcher.write() = Some(watcher); + let result = call(config); + Ok(result) + } + + pub fn config_init + AsRef> (config: C) -> Usually { + // Write initial contents of configuration: + let watcher: &Watch = config.as_ref(); + if watcher.find_file().is_none() { + std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?; + } + // Load config from file: + Ok(if let Some(path) = watcher.find_file() { + let source = std::fs::read_to_string(&path)?; + config_reload(config, source)? + } else { + return Err(format!("config_init: not found").into()) + }) + } + + /// Reload configuration from source. + pub fn config_reload , L: Language> (state: C, src: L) -> Usually { + state.as_ref().config_clear(); + src.each(state, |c, s|config_add(c, s.trim())) + } + + /// Load configuration item. + fn config_add , L: Language> (state: C, src: L) -> Usually { + if let Some(expr) = src.expr()? { + match expr.head()? { + Some("mode") => state.as_ref().modes_add(expr.tail()), + Some("view") => state.as_ref().views_add(expr.tail()), + Some("bind") => state.as_ref().binds_def_keys(expr.tail()), + Some("axis") => state.as_ref().binds_def_axis(expr.tail()), + _ => return Err(format!("config_add: unexpected: {src:?}").into()) + }?; + Ok(state) + } else { + Err(format!("Config::add_one: tried to add empty item").into()) + } + } + + /// Contains timestamped mode, view, and bind definitions. + /// + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// + /// // Default empty config: + /// let config: Config = Default::default(); + /// + /// // Define a binding: + /// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); + /// + /// // Define an axis: + /// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); + /// ``` + #[derive(Default, Debug)] + pub struct Config { + /// Error caught during (re)loading + pub error: RwLock>>, + /// Timestamp + pub stamp: AtomicU64, + /// Active collection of interaction modes. + pub modes: Arc, Arc>>>, + /// Active collection of view definitions. + pub views: Arc, Arc>>>>, + /// Active collection of key bindings. + pub keys: Arc, Arc>>>>>, + /// Active collection of axis bindings. + pub axis: Arc, Arc>>>>, + } + + impl Config { + /// Create a new timestamped config state. + pub fn new () -> Self { + Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() } + } + } +} + /// Configuration watcher and manager. /// /// ``` @@ -177,8 +307,8 @@ impl App { pub struct Watch { /// XDG base directories of running user. pub dirs: BaseDirectories, - /// Watcher - pub watch: RwLock>, + /// File system notifier + pub watcher: RwLock>, /// App configuration pub config: Config, } @@ -193,49 +323,8 @@ impl Watch { /// Create a new watched app configuration /// from a set of XDG base directories, pub fn new (dirs: Option) -> Self { - Self { - dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)), - ..Default::default() - } - } - /// Create, initialize, and watch a new configuration. - pub fn run_watched ( - callback: impl FnOnce(Arc)->T, - dirs: impl Into>, - ) -> Usually { - let watch = Arc::new(Self::new(dirs.into())); - Self::watch(watch.clone(), None)?; - let result = callback(watch); - Ok(result) - } - /// Watch a config's file for changes. - pub fn watch (config: Arc, poll: Option) -> Usually<()> { - let poll = poll.unwrap_or(Duration::from_millis(250)); - let opts = NotifyConfig::default().with_poll_interval(poll); - let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({ - let config = config.clone(); - move|result|{ - match result { - Ok(_events) => if let Err(e) = config_init(config.as_ref()) { - config.error_set(Some(format!("{e:?}").into())); - panic!("{e:?}"); - } else { - //println!("config updated"); - }, - Err(errors) => { - panic!("{errors:?}"); - } - } - } - }, opts)?; - if let Some(path) = config.get_file() { - //println!("watching: {path:?}"); - watcher.watch(&path, RecursiveMode::NonRecursive)?; - *config.as_ref().watch.try_write().unwrap() = Some(watcher); - Ok(()) - } else { - Err(format!("no config path").into()) - } + let dirs = dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)); + Self { dirs, ..Default::default() } } /// Find the config file. fn find_file (&self) -> Option { @@ -249,57 +338,38 @@ impl Watch { fn get_file (&self) -> Option { self.dirs.get_config_file(Self::EDN) } -} - -/// Arranger. -/// -/// ``` -/// let arranger = tek::Project::default(); -/// ``` -#[namespace(ClipPool ClipPool::eval)] -#[derive(Default, Debug)] -pub struct Project { - /// JACK client handle. - pub jack: Jack<'static>, - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Sizer, - /// Display size of clips area - pub size_inner: Sizer, - /// Selected UI element - pub selection: Selection, - /// Contains all recently created clips. - #[cfg(feature = "sequencer")] pub clips: Pool, - /// Contains all recently created clips. - #[cfg(feature = "sampler")] pub samples: Pool, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, - /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scenes_scroll: usize, + /// Watch a config's file for changes. + pub fn watch_config (self: Arc, poll: Option) -> Usually { + let poll = poll.unwrap_or(Duration::from_millis(250)); + let opts = NotifyConfig::default().with_poll_interval(poll); + let call = self.clone().get_watch_config_callback(); + let mut watcher = PollWatcher::new(call, opts)?; + if let Some(path) = self.get_file() { + watcher.watch(&path, RecursiveMode::NonRecursive)?; + Ok(watcher) + } else { + Err(format!("no config path").into()) + } + } + fn get_watch_config_callback (self: Arc) -> impl Fn(Result< + notify_debouncer_full::notify::Event, + notify_debouncer_full::notify::Error, + >) { + let config = self.clone(); + move|result|{ + match result { + Ok(_events) => if let Err(e) = config_init(config.as_ref()) { + config.error_set(Some(format!("{e:?}").into())); + panic!("{e:?}"); + } else { + //println!("config updated"); + }, + Err(errors) => { + panic!("{errors:?}"); + } + } + } + } } fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { @@ -349,609 +419,584 @@ fn tek_jack_event (state: &mut App, event: JackEvent) { } } -impl Project { - pub fn new (jack: &Jack<'static>) -> Self { - Self { - jack: jack.clone(), - ..Default::default() +pub use self::gear::*; mod gear { + use crate::*; + + mod device; pub use self::device::*; + mod midi; pub use self::midi::*; + mod mixer; pub use self::mixer::*; + mod sample; pub use self::sample::*; + + #[cfg(feature = "vst2")] mod vst2; + #[cfg(feature = "vst2")] pub use self::vst2::*; + + #[cfg(feature = "lv2")] mod lv2; + #[cfg(feature = "lv2")] pub use self::lv2::*; + + #[cfg(feature = "lv2_gui")] mod lv2_gui; + #[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*; + + pub struct Junction(T); + + impl Draw for Junction { + fn draw (&self, to: &mut Tui) -> Drawn { + T::KIND.draw(to) } } - - pub fn new_jack (name: impl AsRef) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - jack: Jack::new(name)?, - ..Default::default() - }) - } - - /// Create a new arrangement. - pub fn create ( - jack: &Jack<'static>, - name: Arc, - clock: Clock, - tracks: impl Iterator, - scenes: impl Iterator, - midi_ins: impl Iterator, - midi_outs: impl Iterator, - audio_ins: impl Iterator, - audio_outs: impl Iterator, - ) -> Self { - Self { - jack: jack.clone(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - tracks: tracks.collect(), - scenes: scenes.collect(), - midi_ins: midi_ins.collect(), - midi_outs: midi_outs.collect(), - audio_ins: audio_ins.collect(), - audio_outs: audio_outs.collect(), - size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())), - clock, - name, - ..Default::default() - } - } - - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - - /// Width available to display tracks. - fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - - /// Height taken by visible device slots. - fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } } -impl HasJack<'static> for App { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} -impl HasJack<'static> for Project { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} +pub use self::proj::*; mod proj { + use crate::*; -impl ScenesView for Project { - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.size.w() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} + mod pool; pub use self::pool::*; + mod scene; pub use self::scene::*; + mod select; pub use self::select::*; + mod track; pub use self::track::*; + mod time; pub use self::time::*; -impl HasScenes for Project { - fn scenes_scroll (&self) -> usize { - self.scenes_scroll - } - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } -} + pub trait HasProject: AsRef> + AsMut> {} -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] -pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A clip is selected. - Clip(usize), - /// A sample is selected. - Sample(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip slot (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} + /// Has a loaded project. + impl HasProject for T where T: AsRef> + AsMut> {} -impl Selection { - - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match &s.clips.get(*track) { - None => format!("T{track} S{scene}: Empty"), - Some(clip) => format!("T{track} S{scene} C{}", - clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()), - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - -} - -#[cfg(feature = "scene")] -impl Selection { - - pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - - pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - - pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - - pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - -} - -#[cfg(feature = "track")] -impl Selection { - - pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - - pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - - pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - - pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } - -} - -impl SelectionModel for T {} - -pub trait SelectionModel: HasSelection { - /// Get the active track - #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.read().get(self.selection().track()?).cloned() - } -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Pool, -} - -impl Scene { - pub const DEFAULT_HEIGHT: usize = 2; - /// Get pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.read().iter().fold(0, |a, p|{ - a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0)) - }) - } - /// True if all clips in scene are currently playing on given tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - // Not "playing" if there are no clips - if !self.clips.read().iter().any(|clip|clip.read().is_some()) { - return false - } - // Playing if at least 1 clip is playing - self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read() == *c.read() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } -} - -impl ScenesView for App { - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(self.w_side()) - } - fn w_side (&self) -> u16 { - 20 - } - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } -} - -impl HasScenes for App { - fn scenes_scroll (&self) -> usize { - self.project.scenes_scroll() - } - fn scenes (&self) -> &Vec { - self.project.scenes() - } - fn scenes_mut (&mut self) -> &mut Vec { - self.project.scenes_mut() - } -} - -impl App { - /// Return reference to content browser if open. + /// Arranger. /// /// ``` - /// assert_eq!(tek::App::default().browser(), None); + /// let arranger = tek::Project::default(); /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + #[namespace(ClipPool ClipPool::eval)] + #[derive(Default, Debug)] + pub struct Project { + /// JACK client handle. + pub jack: Jack<'static>, + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Sizer, + /// Display size of clips area + pub size_inner: Sizer, + /// Selected UI element + pub selection: Selection, + /// Contains all recently created clips. + #[cfg(feature = "sequencer")] pub clips: Pool, + /// Contains all recently created clips. + #[cfg(feature = "sampler")] pub samples: Pool, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Last track number (to avoid duplicate port names) + #[cfg(feature = "track")] pub track_last: usize, + /// List of tracks + #[cfg(feature = "track")] pub tracks: Vec, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub scenes_scroll: usize, } -} -/// Has a MIDI editor. -/// -/// ``` -/// use tek::{*, tengri::*}; -/// -/// struct Test(Option); -/// impl_as_ref!(Option: |self: Test|&self.0); -/// impl_as_mut!(Option: |self: Test|&mut self.0); -/// -/// let mut host = Test(Some(MidiEditor::default())); -/// let _ = host.editor(); -/// let _ = host.editor_mut(); -/// let _ = host.is_editing(); -/// let _ = host.editor_w(); -/// let _ = host.editor_h(); -/// ``` -impl HasEditor for T where T: - AsRef> + AsMut> {} -pub trait HasEditor: - AsRef> + AsMut> -{ - fn editor (&self) -> &Option { - self.as_ref() - } - fn editor_mut (&mut self) -> &mut Option { - self.as_mut() - } -} - -/// Has a pair of buffers for MIDI IO. -pub trait HasMidiBuffers { - fn note_buf_mut (&mut self) -> &mut Vec; - fn midi_buf_mut (&mut self) -> &mut Vec>>; -} - -/// Can have a currently playing clip. -pub trait HasPlayClip: HasClock { - fn reset (&self) -> bool; - fn reset_mut (&mut self) -> &mut bool; - fn play_clip (&self) -> &Option<(Moment, Option>>)>; - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn next_clip (&self) -> &Option<(Moment, Option>>)>; - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; -} - -/// Has a MIDI sequencer. -impl HasSequencer for T where T: AsRef + AsMut {} -pub trait HasSequencer: AsRef + AsMut { - fn sequencer (&self) -> &Sequencer { - self.as_ref() - } - fn sequencer_mut (&mut self) -> &mut Sequencer { - self.as_mut() - } -} - -/// Has a clock. -pub trait HasClock: AsRef + AsMut { - fn clock (&self) -> &Clock { - self.as_ref() - } - fn clock_mut (&mut self) -> &mut Clock { - self.as_mut() - } -} -impl HasClock for T where T: AsRef + AsMut {} - -/// Has a chain of devices. -pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { - self.as_ref() - } - fn devices_mut (&mut self) -> &mut Vec { - self.as_mut() - } -} - -pub trait HasProject: AsRef> + AsMut> {} - -/// Has a loaded project. -impl HasProject for T where T: AsRef> + AsMut> {} - -pub trait HasScene: AsRefOpt + AsMutOpt { - fn scene (&self) -> Option<&Scene> { - self.as_ref_opt() - } - fn scene_mut (&mut self) -> Option<&mut Scene> { - self.as_mut_opt() - } -} -impl HasScene for T where T: AsRefOpt + AsMutOpt + Send + Sync {} - -pub trait HasScenes { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scenes_scroll (&self) -> usize { - 0 - } -} - -impl HasSelection for T where T: AsRef + AsMut {} -pub trait HasSelection: AsRef + AsMut { - fn selection (&self) -> &Selection { - self.as_ref() - } - fn selection_mut (&mut self) -> &mut Selection { - self.as_mut() - } -} - -impl HasTrack for T where T: AsRefOpt + AsMutOpt {} -pub trait HasTrack: AsRefOpt + AsMutOpt { - fn track (&self) -> Option<&Track> { - self.as_ref_opt() - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.as_mut_opt() - } -} - -impl HasTracks for T where T: AsRef> + AsMut> + HasClock + HasTrackScroll {} -pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrackScroll { - /// Read-only reference to collection of [Track]s. - fn tracks (&self) -> &Vec { - self.as_ref() - } - /// Mutable reference to collection of [Track]s. - fn tracks_mut (&mut self) -> &mut Vec { - self.as_mut() - } -} - -pub trait HasTrackScroll { - fn track_scroll (&self) -> usize; - fn tracks_last (&self) -> usize; - fn tracks_last_mut (&mut self) -> &mut usize; -} -impl HasTrackScroll for &mut Project { - fn track_scroll (&self) -> usize { - self.track_scroll - } - fn tracks_last (&self) -> usize { - self.track_last - } - fn tracks_last_mut (&mut self) -> &mut usize { - &mut self.track_last - } -} -impl HasTrackScroll for Project { - fn track_scroll (&self) -> usize { - self.track_scroll - } - fn tracks_last (&self) -> usize { - self.track_last - } - fn tracks_last_mut (&mut self) -> &mut usize { - &mut self.track_last - } -} -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { - self.project.track_scroll() - } - fn tracks_last (&self) -> usize { - self.project.track_last - } - fn tracks_last_mut (&mut self) -> &mut usize { - &mut self.project.track_last - } -} - -pub trait HasClip { - fn clip_slot (&self) -> Option>; - fn clip (&self) -> Option>>>> { - self.clip_slot().map(|slot|slot.read()) - } - fn clip_mut (&self) -> Option>>>> { - self.clip_slot().map(|slot|slot.write()) - } -} -impl HasClip for App { - fn clip_slot (&self) -> Option> { - self.project.clip_slot() - } -} -impl HasClip for Project { - fn clip_slot (&self) -> Option> { - match self.selection { - Selection::Clip(index) => - Some(self.clips.slot(index)), - Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) => - Some(scene.clips.slot(track)), - _ => - None + impl Project { + pub fn new (jack: &Jack<'static>) -> Self { + Self { jack: jack.clone(), ..Default::default() } + } + pub fn new_jack (name: impl AsRef) -> Usually { + let name = name.as_ref(); + Ok(Self { name: name.into(), jack: Jack::new(name)?, ..Default::default() }) + } + /// Create a new arrangement. + pub fn create ( + jack: &Jack<'static>, + name: Arc, + clock: Clock, + tracks: impl Iterator, + scenes: impl Iterator, + midi_ins: impl Iterator, + midi_outs: impl Iterator, + audio_ins: impl Iterator, + audio_outs: impl Iterator, + ) -> Self { + Self { + jack: jack.clone(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + tracks: tracks.collect(), + scenes: scenes.collect(), + midi_ins: midi_ins.collect(), + midi_outs: midi_outs.collect(), + audio_ins: audio_ins.collect(), + audio_outs: audio_outs.collect(), + size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())), + clock, + name, + ..Default::default() + } } } + + impl HasJack<'static> for Project { + fn jack (&self) -> &Jack<'static> { + &self.jack + } + } + + impl HasClip for Project { + fn clip_slot (&self) -> Option> { + match self.selection { + Selection::Clip(index) => + Some(self.clips.slot(index)), + Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) => + Some(scene.clips.slot(track)), + _ => + None + } + } + } + + impl HasClipsSize for Project { + fn clips_size (&self) -> &Sizer { + &self.size_inner + } + } + + #[cfg(feature = "cli")] + /// Command to launch into new project from CLI. + #[derive(Debug, Clone, Parser, Default)] + pub struct ProjectInit { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to creat + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + } + + #[cfg(feature = "cli")] + impl ProjectInit { + pub fn init (&self) -> Usually { + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?; + let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?; + let inputs = [].into_iter() + .chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter()) + .chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter()); + let outputs = [].into_iter() + .chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter()) + .chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter()); + let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?, + [].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(), + inputs, outputs); + proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?; + proj.scenes_add_many(self.scenes.unwrap_or(0))?; + Ok(proj) + } + } + } -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} +#[cfg(feature = "cli")] pub use self::cli::*; +#[cfg(feature = "cli")] mod cli { + use super::*; -pub trait HasClipsSize { - /// Size of clips area. - fn clips_size (&self) -> &Sizer; - /// Draw content annotated with value of [Self::clips_size]. - fn with_clips_size (&self, show: bool, content: impl Draw) -> impl Draw { - let size = self.clips_size(); - above( - when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()), - size.of(content.align_c().full_wh()) - ) + /// Banner. + pub(crate) const HEADER: &'static str = r#" + ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ + █ █▀ █▀▀▄ ~ heatwave is the new darkwave ~ + ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; + + pub fn show_version () { + println!("versions aint real man"); } -} -impl HasClipsSize for App { - fn clips_size (&self) -> &Sizer { - &self.project.size_inner + + pub fn main_cli (config: Arc) -> Usually<()> { + Cli::parse().run(Some(config.into())) } -} -impl HasClipsSize for Project { - fn clips_size (&self) -> &Sizer { - &self.size_inner + + /// The command-line interface descriptor. + /// + /// ``` + /// let cli: tek::Cli = Default::default(); + /// + /// use clap::CommandFactory; + /// tek::Cli::command().debug_assert(); + /// ``` + #[derive(Parser, Debug, Default)] + #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] + pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] + pub action: Action, + + /// Record data for performance flamegraph. + #[arg(long)] + trace: bool, + } + + /// Command-line configuration. + impl Cli { + pub fn run (&self, mut config: Option>) -> Usually<()> { + self.action.run( + config.unwrap_or_else(||Arc::new(Watch::new(None))), + self.trace + ) + } + } + + /// Application modes that can be passed to the mommand line interface. + /// + /// ``` + /// let action: tek::Action = Default::default(); + /// ``` + #[derive(Debug, Clone, Subcommand, Default)] + pub enum Action { + /// Continue where you left off + #[default] Resume, + /// Run headlessly in current session. + Headless, + /// Show status of current session. + Status, + /// List known sessions. + List, + /// Continue work in a copy of the current session. + Fork, + /// Create a new empty session. + New(ProjectInit), + /// Import media as new session. + Import, + /// Show configuration. + Config, + /// Show version. + Version, + } + + impl Action { + fn run (&self, watch: Arc, _trace: bool) -> Usually<()> { + use Action::*; + match self { + Version => show_version(), + Config => watch.config_print(), + Resume => todo!("resume session"), + List => todo!("list sessions"), + New(sesh) => Exit::run(|exit|Tui::run_main( + exit.clone(), + { + #[allow(unused_mut)] + let mut app = App::new(Some(exit), sesh.init()?, watch, Some(vec![ + "Menu".into() + ])); + app.dialog = Dialog::welcome(); + #[cfg(feature = "prof2")] { + if trace { + app.guard = Some({ + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + //let tracy = tracing_tracy::TracyLayer::default(); + let (flame, _guard) = tracing_flame::FlameLayer::with_file("./tracing.folded").unwrap(); + //let registry = tracing_subscriber::registry().with(flame).init(); + //tracing::subscriber::set_global_default(registry)?; + tracing::subscriber::set_global_default(tracing_subscriber::registry().with(flame))?; + _guard + }) + } + } + Arc::new(RwLock::new(app)) + })).map(|_|())?, + _ => todo!() + } + Ok(()) + } + } + + pub fn print_status (project: &Project) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration } } -pub struct Junction(T); +impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl Draw for Junction { +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a>: Iterator + Send + Sync + 'a {} + impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} + } +} + +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); +def_sizes_iter!(PortsSizes => Arc, [Connect]); + +impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); + +primitive!(isize: try_to_isize); +primitive!(u16: try_to_u16); +primitive!(u8: try_to_u8); +primitive!(usize: try_to_usize); + +impl_as_ref!(Config: |self: Config| self); +impl_as_ref!(Config: |self: Watch| &self.config); +impl_as_mut!(Config: |self: Watch| &mut self.config); +impl_as_ref!(Watch: |self: Watch| &self); + +impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) }); +impl_default!(MenuItem: Self::new("", [])); + +impl_as_mut!(Option: |self: Project| &mut self.editor); +impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); +impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); +impl_as_ref!(Option: |self: Project| &self.editor); +impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); +impl_as_ref_opt!(Track: |self: Project| self.selected_track()); +impl_has!(Jack<'static>: |self: Project| self.jack); +impl_has!(Selection: |self: Project| self.selection); +impl_has!(Sizer: |self: Project| self.size); +impl_has!(Vec: |self: Project| self.midi_ins); +impl_has!(Vec: |self: Project| self.midi_outs); + +impl_as_ref!(Config: |self: App|self.watch.config.as_ref()); +impl_as_mut!(Option: |self: App|self.project.as_mut()); +impl_as_mut!(Option: |self: App|todo!()); +impl_as_ref!(Option: |self: App|self.project.as_ref()); +impl_as_ref!(Option: |self: App|todo!()); +impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt()); +impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt()); +impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt()); +impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt()); +impl_audio!(App: tek_jack_process, tek_jack_event); + +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Pool: |self: App|self.project.clips); +impl_has!(Pool: |self: App|self.project.samples); +impl_has!(Selection: |self: App|self.project.selection); +impl_has!(Sizer: |self: App|self.size); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); + +impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x }); +impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x }); +impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x }); +impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x }); +impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); +impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); +impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x }); +impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); + +tui_keys!(self: App, input { + #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); + self.dispatch_bind(input) +}); + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl Draw for App { fn draw (&self, to: &mut Tui) -> Drawn { - T::KIND.draw(to) + #[cfg(feature = "prof2")] profiling::scope!("App::draw"); + self.perf.cycle(&mut |_|{ + draw_error(to, self.error.as_ref())?; + draw_mode_stack(to, &self)?; + view_debug(&*self.stack.read(), &self.perf, &self.size).draw(to)?; + #[cfg(feature = "prof2")] profiling::finish_frame!(); + Ok(Some(to.area().into())) + }) + } +} + +pub trait DispatchBind: HasBinds + HasModeStack + HasModes + Namespace { + fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> { + if let Some(bindings) = self.dispatch_bind_match(input) { + return self.dispatch_bind_exec(bindings) + } + Ok(()) + } + /// Find command bindings that match the ,nput. + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// // Configure modes: + /// let app = config_reload(App::default(), stringify!( + /// (axis Y @up @down) + /// (bind Confirm (@enter confirm)) + /// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm)) + /// )).unwrap(); + /// println!("Modes: {:#?}", app.watch.config.modes); + /// + /// // No mode is active: + /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); + /// + /// // Push mode onto stack: + /// app.mode_stack_push("Test"); + /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); + /// ``` + fn dispatch_bind_match (&self, input: &TuiEvent) -> Option>]>> { + for name in self.mode_stack().read().iter().rev() { + if let Some(mode) = self.modes_get(name).as_ref().map(Arc::clone) { + if let Some(binds) = self.dispatch_bind_match_axis(mode.as_ref(), input) { + return Some(binds) + } + if let Some(binds) = self.dispatch_bind_match_keys(mode.as_ref(), input) { + return Some(binds) + } + } + } + None + } + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let app = config_reload(App::default(), stringify!( + /// (axis Y @up @down) (mode Test (axis Y Index)) + /// )).unwrap(); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); + /// app.mode_stack_push("Test"); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); + /// ``` + fn dispatch_bind_match_axis (&self, mode: &Mode, input: &TuiEvent) + -> Option>]>> + { + let axis = self.binds_axis().read(); + for (ctrl, _param) in mode.axis.iter() { + if let Some(axis) = axis.get(ctrl) { + if *input == axis.0 { + return Some([Binding { + commands: [].into(), ..Default::default() + }].into()) + } else if *input == axis.1 { + return Some([Binding { + commands: [].into(), ..Default::default() + }].into()) + } + } + } + None + } + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let app = config_reload(App::default(), stringify!( + /// (bind Confirm (@enter confirm)) (mode Test (bind Confirm (@escape Cancel))) + /// )).unwrap(); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); + /// app.mode_stack_push("Test"); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); + /// ``` + fn dispatch_bind_match_keys (&self, mode: &Mode, input: &TuiEvent) + -> Option>]>> + { + for id in mode.keys.iter() { + println!("{input:?} {id:?}"); + if let Some(event_map) = self.binds_find(id) + && let Some(bindings) = event_map.query(input) { + return Some(bindings.into()) + } + if let Ok(Some(expr)) = id.expr() + && let Ok(Some(head)) = expr.head() + && *input == head.into() + && let Ok(Some(tail)) = expr.tail() { + return Some([Binding { + commands: [tail.into()].into(), ..Default::default() + }].into()) + } + } + None + } + fn dispatch_bind_exec (&mut self, bindings: Arc<[Binding>]>) -> Usually<()> { + for binding in bindings.iter() { + for src in binding.commands.iter() { + if let Some(command) = Namespace::::namespace(self, &src)? { + self.cmd_dispatch(command)?; + } + } + } + Ok(()) + } + fn cmd_dispatch (&mut self, command: C) -> Usually<()>; +} + +impl DispatchBind for App { + fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> { + match command.clone().dispatch(self) { + Ok(undo) => { self.history.push((command, undo)); Ok(()) }, + Err(err) => { self.history.push((command, None)); Err(err) }, + } } } @@ -1089,6 +1134,17 @@ impl Dialog { } +impl App { + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } +} + /// Browses for files to load/save. /// /// ``` @@ -1190,608 +1246,3 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { files.sort(); Ok((subdirs, files)) } - -pub trait DispatchBind: HasBinds + HasModeStack + HasModes + Namespace { - fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> { - if let Some(bindings) = self.dispatch_bind_match(input) { - return self.dispatch_bind_exec(bindings) - } - Ok(()) - } - /// Find command bindings that match the ,nput. - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// // Configure modes: - /// let app = config_reload(App::default(), stringify!( - /// (axis Y @up @down) - /// (bind Confirm (@enter confirm)) - /// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm)) - /// )).unwrap(); - /// println!("Modes: {:#?}", app.watch.config.modes); - /// - /// // No mode is active: - /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); - /// - /// // Push mode onto stack: - /// app.mode_stack_push("Test"); - /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); - /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); - /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); - /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); - /// ``` - fn dispatch_bind_match (&self, input: &TuiEvent) -> Option>]>> { - for name in self.mode_stack().read().iter().rev() { - if let Some(mode) = self.modes_get(name).as_ref().map(Arc::clone) { - if let Some(binds) = self.dispatch_bind_match_axis(mode.as_ref(), input) { - return Some(binds) - } - if let Some(binds) = self.dispatch_bind_match_keys(mode.as_ref(), input) { - return Some(binds) - } - } - } - None - } - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// let app = config_reload(App::default(), stringify!( - /// (axis Y @up @down) (mode Test (axis Y Index)) - /// )).unwrap(); - /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); - /// app.mode_stack_push("Test"); - /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); - /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); - /// ``` - fn dispatch_bind_match_axis (&self, mode: &Mode, input: &TuiEvent) -> Option>]>> { - let axis = self.binds_axis().read(); - for (ctrl, _param) in mode.axis.iter() { - if let Some(axis) = axis.get(ctrl) { - if *input == axis.0 { - return Some([Binding { commands: [].into(), ..Default::default() }].into()) - } else if *input == axis.1 { - return Some([Binding { commands: [].into(), ..Default::default() }].into()) - } - } - } - None - } - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// let app = config_reload(App::default(), stringify!( - /// (bind Confirm (@enter confirm)) (mode Test (bind Confirm (@escape Cancel))) - /// )).unwrap(); - /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); - /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); - /// app.mode_stack_push("Test"); - /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); - /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); - /// ``` - fn dispatch_bind_match_keys (&self, mode: &Mode, input: &TuiEvent) -> Option>]>> { - for id in mode.keys.iter() { - println!("{input:?} {id:?}"); - if let Some(event_map) = self.binds_find(id) - && let Some(bindings) = event_map.query(input) { - return Some(bindings.into()) - } - if let Ok(Some(expr)) = id.expr() - && let Ok(Some(head)) = expr.head() - && *input == head.into() - && let Ok(Some(tail)) = expr.tail() { - return Some([Binding { commands: [tail.into()].into(), ..Default::default() }].into()) - } - } - None - } - fn dispatch_bind_exec (&mut self, bindings: Arc<[Binding>]>) -> Usually<()> { - for binding in bindings.iter() { - for src in binding.commands.iter() { - if let Some(command) = Namespace::::namespace(self, &src)? { - self.cmd_dispatch(command)?; - } - } - } - Ok(()) - } - fn cmd_dispatch (&mut self, command: C) -> Usually<()>; -} - -impl DispatchBind for App { - fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> { - match command.clone().dispatch(self) { - Ok(undo) => { self.history.push((command, undo)); Ok(()) }, - Err(err) => { self.history.push((command, None)); Err(err) }, - } - } -} - -pub use self::conf::*; mod conf { - use crate::*; - - mod axis; pub use self::axis::*; - mod bind; pub use self::bind::*; - mod lang; pub use self::lang::*; - mod mode; pub use self::mode::*; - mod view; pub use self::view::*; - - pub trait HasError { - fn error (&self) -> &RwLock>; - fn error_set (&self, e: Option) { - *self.error().write() = e; - } - } - impl> HasError> for T { - fn error (&self) -> &RwLock>> { - &self.as_ref().error - } - } - impl HasError> for Arc { - fn error (&self) -> &RwLock>> { - &self.as_ref().config.error - } - } - - pub trait HasConfig { - fn config (&self) -> &Config; - /// Print the configuration. - fn config_print (&self) { - let config = self.config(); - print_config_views(config); - print_config_binds(config); - print_config_modes(config); - } - /// Make this configuration empty. - fn config_clear (&self) { - let config = self.config(); - config.modes_clear(); - config.views_clear(); - config.binds_clear(); - config.stamp.store(quanta::Clock::new().raw(), Relaxed); - } - } - impl HasConfig for App { - fn config (&self) -> &Config { - &self.watch.config - } - } - impl HasConfig for Watch { - fn config (&self) -> &Config { - &self.config - } - } - impl HasConfig for Config { - fn config (&self) -> &Config { - self - } - } - - /// Default configuration contents. - const CONFIG_DEFAULT: &'static str = include_str!("tek.edn"); - - pub fn config_init + AsRef> (config: C) -> Usually { - // Write initial contents of configuration: - let watcher: &Watch = config.as_ref(); - if watcher.find_file().is_none() { - std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?; - } - // Load config from file: - Ok(if let Some(path) = watcher.find_file() { - config_reload(config, std::fs::read_to_string(&path)?)? - } else { - return Err(format!("config_init: not found").into()) - }) - } - - /// Reload configuration from source. - pub fn config_reload , L: Language> (state: C, src: L) -> Usually { - state.as_ref().config_clear(); - src.each(state, |c, s|config_add(c, s.trim())) - } - - /// Load configuration item. - fn config_add , L: Language> (state: C, src: L) -> Usually { - if let Some(expr) = src.expr()? { - match expr.head()? { - Some("mode") => state.as_ref().modes_add(expr.tail()), - Some("view") => state.as_ref().views_add(expr.tail()), - Some("bind") => state.as_ref().binds_def_keys(expr.tail()), - Some("axis") => state.as_ref().binds_def_axis(expr.tail()), - _ => return Err(format!("config_add: unexpected: {src:?}").into()) - }?; - Ok(state) - } else { - Err(format!("Config::add_one: tried to add empty item").into()) - } - } - - /// Contains timestamped mode, view, and bind definitions. - /// - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// - /// // Default empty config: - /// let config: Config = Default::default(); - /// - /// // Define a binding: - /// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); - /// - /// // Define an axis: - /// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); - /// ``` - #[derive(Default, Debug)] - pub struct Config { - /// Error caught during (re)loading - pub error: RwLock>>, - /// Timestamp - pub stamp: AtomicU64, - /// Active collection of interaction modes. - pub modes: Arc, Arc>>>, - /// Active collection of view definitions. - pub views: Arc, Arc>>>>, - /// Active collection of key bindings. - pub keys: Arc, Arc>>>>>, - /// Active collection of axis bindings. - pub axis: Arc, Arc>>>>, - } - - impl Config { - /// Create a new timestamped config state. - pub fn new () -> Self { - Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() } - } - } -} - -pub use self::gear::*; mod gear { - mod device; pub use self::device::*; - mod midi; pub use self::midi::*; - mod mixer; pub use self::mixer::*; - mod pool; pub use self::pool::*; - mod sample; pub use self::sample::*; - mod track; pub use self::track::*; - - #[cfg(feature = "lv2")] mod lv2; - #[cfg(feature = "lv2")] pub use self::lv2::*; - - #[cfg(feature = "lv2_gui")] mod lv2_gui; - #[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*; - - #[cfg(feature = "vst2")] mod vst2; - #[cfg(feature = "vst2")] pub use self::vst2::*; -} - -#[cfg(feature = "cli")] pub use self::cli::*; -#[cfg(feature = "cli")] mod cli { - use super::*; - - /// Banner. - pub(crate) const HEADER: &'static str = r#" - ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ █▀ █▀▀▄ ~ heatwave is the new darkwave ~ - ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; - - pub fn show_version () { - println!("versions aint real man"); - } - - pub fn main_cli (config: Arc) -> Usually<()> { - Cli::parse().run(Some(config.into())) - } - - /// The command-line interface descriptor. - /// - /// ``` - /// let cli: tek::Cli = Default::default(); - /// - /// use clap::CommandFactory; - /// tek::Cli::command().debug_assert(); - /// ``` - #[derive(Parser, Debug, Default)] - #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] - pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] - pub action: Action, - - /// Record data for performance flamegraph. - #[arg(long)] - trace: bool, - } - - /// Command-line configuration. - impl Cli { - pub fn run (&self, mut config: Option>) -> Usually<()> { - self.action.run( - config.unwrap_or_else(||Arc::new(Watch::new(None))), - self.trace - ) - } - } - - /// Application modes that can be passed to the mommand line interface. - /// - /// ``` - /// let action: tek::Action = Default::default(); - /// ``` - #[derive(Debug, Clone, Subcommand, Default)] - pub enum Action { - /// Continue where you left off - #[default] Resume, - /// Run headlessly in current session. - Headless, - /// Show status of current session. - Status, - /// List known sessions. - List, - /// Continue work in a copy of the current session. - Fork, - /// Create a new empty session. - New(ProjectInit), - /// Import media as new session. - Import, - /// Show configuration. - Config, - /// Show version. - Version, - } - - impl Action { - fn run (&self, watch: Arc, _trace: bool) -> Usually<()> { - use Action::*; - match self { - Version => show_version(), - Config => watch.config_print(), - Resume => todo!("resume session"), - List => todo!("list sessions"), - New(sesh) => Exit::run(|exit|Tui::run_main( - exit.clone(), - { - #[allow(unused_mut)] - let mut app = App::new(Some(exit), sesh.init()?, watch, "Menu"); - app.dialog = Dialog::welcome(); - #[cfg(feature = "prof2")] { - if trace { - app.guard = Some({ - use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - //let tracy = tracing_tracy::TracyLayer::default(); - let (flame, _guard) = tracing_flame::FlameLayer::with_file("./tracing.folded").unwrap(); - //let registry = tracing_subscriber::registry().with(flame).init(); - //tracing::subscriber::set_global_default(registry)?; - tracing::subscriber::set_global_default(tracing_subscriber::registry().with(flame))?; - _guard - }) - } - } - Arc::new(RwLock::new(app)) - })).map(|_|())?, - _ => todo!() - } - Ok(()) - } - } - - /// Command to launch into new project from CLI. - #[derive(Debug, Clone, Parser, Default)] - pub struct ProjectInit { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to creat - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - } - - impl ProjectInit { - pub fn init (&self) -> Usually { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?; - let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?; - let inputs = [].into_iter() - .chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter()) - .chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter()); - let outputs = [].into_iter() - .chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter()) - .chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter()); - let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?, - [].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(), - inputs, outputs); - proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?; - proj.scenes_add_many(self.scenes.unwrap_or(0))?; - Ok(proj) - } - } - - pub fn print_status (project: &Project) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration - } -} - -/// Define and implement a unit of time -macro_rules! impl_time_unit { - ($T:ident) => { - impl TimeUnit for $T {} - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl_from!($T: |value: f64| { Self(value.into()) }); - impl_from!($T: |value: usize| { Self((value as f64).into()) }); - impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } - impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } - impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } - impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - impl $T { - pub fn get (&self) -> f64 { - self.0.load(Relaxed) - } - pub fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Relaxed); - old - } - } - } -} - -impl_time_unit!(SampleCount); -impl_time_unit!(SampleRate); -impl_time_unit!(Microsecond); -impl_time_unit!(Quantize); -impl_time_unit!(Ppq); -impl_time_unit!(Pulse); -impl_time_unit!(Bpm); -impl_time_unit!(LaunchSync); - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a>: Iterator + Send + Sync + 'a {} - impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} - } -} - -def_sizes_iter!(ScenesSizes => Scene); -def_sizes_iter!(TracksSizes => Track); -def_sizes_iter!(PortsSizes => Arc, [Connect]); - -impl_has!(Clock: |self: Track|self.sequencer.clock); -impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); - -primitive!(isize: try_to_isize); -primitive!(u16: try_to_u16); -primitive!(u8: try_to_u8); -primitive!(usize: try_to_usize); - -impl_as_ref!(Config: |self: Config| self); -impl_as_ref!(Config: |self: Watch| &self.config); -impl_as_mut!(Config: |self: Watch| &mut self.config); -impl_as_ref!(Watch: |self: Watch| &self); - -impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) }); -impl_default!(MenuItem: Self::new("", [])); - -impl_as_mut!(Option: |self: Project| &mut self.editor); -impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); -impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); -impl_as_ref!(Option: |self: Project| &self.editor); -impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); -impl_as_ref_opt!(Track: |self: Project| self.selected_track()); -impl_has!(Clock: |self: Project| self.clock); -impl_has!(Jack<'static>: |self: Project| self.jack); -impl_has!(Selection: |self: Project| self.selection); -impl_has!(Sizer: |self: Project| self.size); -impl_has!(Vec: |self: Project| self.midi_ins); -impl_has!(Vec: |self: Project| self.midi_outs); - -impl_as_ref!(Config: |self: App|self.watch.config.as_ref()); -impl_as_mut!(Option: |self: App|self.project.as_mut()); -impl_as_mut!(Option: |self: App|todo!()); -impl_as_ref!(Option: |self: App|self.project.as_ref()); -impl_as_ref!(Option: |self: App|todo!()); -impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt()); -impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt()); -impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt()); -impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt()); -impl_audio!(App: tek_jack_process, tek_jack_event); - -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Pool: |self: App|self.project.clips); -impl_has!(Pool: |self: App|self.project.samples); -impl_has!(Selection: |self: App|self.project.selection); -impl_has!(Sizer: |self: App|self.size); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); - -impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x }); -impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x }); -impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x }); -impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x }); -impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); -impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); -impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x }); -impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); - -tui_keys!(self: App, input { - #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); - self.dispatch_bind(input) -}); - -/// The [Draw] implementation for [App] handles the loaded view, -/// which is defined in terms of [dizzle] DSL. -/// -/// If there is an error, the error is displayed. FIXME: overlay it -/// Then, every top-level form of the DSL description is rendered. -impl Draw for App { - fn draw (&self, to: &mut Tui) -> Drawn { - #[cfg(feature = "prof2")] profiling::scope!("App::draw"); - self.perf.cycle(&mut |_|{ - draw_error(to, self.error.as_ref())?; - draw_mode_stack(to, &self)?; - view_debug(&*self.stack.read(), &self.perf, &self.size).draw(to)?; - #[cfg(feature = "prof2")] profiling::finish_frame!(); - Ok(Some(to.area().into())) - }) - } -} diff --git a/tengri b/tengri index 65f7948e..3e845dbe 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 65f7948e9c0b5ed535d13c354ef5f4150a801f01 +Subproject commit 3e845dbe83f679d11e431a236cc75f2e03792c7f