use crate::*; use std::fmt::Write; pub trait HasClock: Send + Sync { fn clock (&self) -> &Clock; fn clock_mut (&mut self) -> &mut Clock; } impl> HasClock for T { fn clock (&self) -> &Clock { self.get() } fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } } #[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 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) -> &BeatsPerMinute { &self.timebase.bpm } /// Current MIDI resolution pub fn ppq (&self) -> &PulsesPerQuaver { &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.read().unwrap().is_none() } /// Is currently playing? pub fn is_rolling (&self) -> bool { self.started.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.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.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) -> TicksIterator { self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) } } impl Clock { fn _todo_provide_u32 (&self) -> u32 { todo!() } fn _todo_provide_opt_u32 (&self) -> Option { todo!() } fn _todo_provide_f64 (&self) -> f64 { todo!() } } impl Command for ClockCommand { fn execute (&self, state: &mut T) -> Perhaps { self.execute(state.clock_mut()) // awesome } } def_command!(ClockCommand: |clock: Clock| { SeekUsec { usec: f64 } => { clock.playhead.update_from_usec(*usec); Ok(None) }, SeekSample { sample: f64 } => { clock.playhead.update_from_sample(*sample); Ok(None) }, SeekPulse { pulse: f64 } => { clock.playhead.update_from_pulse(*pulse); Ok(None) }, SetBpm { bpm: f64 } => Ok(Some( Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), SetQuant { quant: f64 } => Ok(Some( Self::SetQuant { quant: clock.quant.set(*quant) })), SetSync { sync: f64 } => Ok(Some( Self::SetSync { sync: clock.sync.set(*sync) })), Play { position: Option } => { clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, Pause { position: Option } => { clock.pause_at(*position)?; Ok(None) }, TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { clock.pause_at(Some(*position))?; None } else { clock.play_from(Some(*position))?; None }), }); pub fn view_transport ( play: bool, bpm: Arc>, beat: Arc>, time: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( Fill::XY(Align::w(button_play_pause(play))), Fill::XY(Align::e(row!( FieldH(theme, "BPM", bpm), FieldH(theme, "Beat", beat), FieldH(theme, "Time", time), ))) ))) } pub fn view_status ( sel: Option>, sr: Arc>, buf: Arc>, lat: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), Fill::XY(Align::e(row!( FieldH(theme, "SR", sr), FieldH(theme, "Buf", buf), FieldH(theme, "Lat", lat), ))) ))) } pub(crate) fn button_play_pause (playing: bool) -> impl Content { let compact = true;//self.is_editing(); Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, Either::new(compact, Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, Tui::fg(Rgb(0, 255, 0), " PLAYING "), Tui::fg(Rgb(255, 128, 0), " STOPPED "))) )), Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) )) ) ) } #[derive(Debug)] pub struct ViewCache { pub sr: Memo, String>, pub buf: Memo, String>, pub lat: Memo, String>, pub bpm: Memo, String>, pub beat: Memo, String>, pub time: Memo, String>, } impl Default for ViewCache { fn default () -> Self { let mut beat = String::with_capacity(16); let _ = write!(beat, "{}", Self::BEAT_EMPTY); let mut time = String::with_capacity(16); let _ = write!(time, "{}", Self::TIME_EMPTY); let mut bpm = String::with_capacity(16); let _ = write!(bpm, "{}", Self::BPM_EMPTY); Self { beat: Memo::new(None, beat), time: Memo::new(None, time), bpm: Memo::new(None, bpm), sr: Memo::new(None, String::with_capacity(16)), buf: Memo::new(None, String::with_capacity(16)), lat: Memo::new(None, String::with_capacity(16)), } } } impl ViewCache { pub const BEAT_EMPTY: &'static str = "-.-.--"; pub const TIME_EMPTY: &'static str = "-.---s"; pub const BPM_EMPTY: &'static str = "---.---"; //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) //-> Arc> //{ //let data = (track, tracks); //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); //cache.read().unwrap().trks.view.clone() //} //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) //-> impl Content //{ //let data = (scene, scenes); //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) //} pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { let rate = clock.timebase.sr.get(); let chunk = clock.chunk.load(Relaxed) as f64; let lat = chunk / rate * 1000.; let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); let mut cache = cache.write().unwrap(); cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); cache.sr.update(Some((compact, rate)), |buf,_,_|{ buf.clear(); if compact { write!(buf, "{:.1}kHz", rate / 1000.) } else { write!(buf, "{:.0}Hz", rate) } }); if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { let pulse = clock.timebase.usecs_to_pulse(now); let time = now/1000000.; let bpm = clock.timebase.bpm.get(); cache.beat.update(Some(pulse), |buf, _, _|{ buf.clear(); clock.timebase.format_beats_1_to(buf, pulse) }); cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); } else { cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); } } //pub fn view_h2 (&self) -> impl Content { //let cache = self.project.clock.view_cache.clone(); //let cache = cache.read().unwrap(); //add(&Fixed::x(15, Align::w(Bsp::s( //FieldH(theme, "Beat", cache.beat.view.clone()), //FieldH(theme, "Time", cache.time.view.clone()), //)))); //add(&Fixed::x(13, Align::w(Bsp::s( //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), //)))); //add(&Fixed::x(12, Align::w(Bsp::s( //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), //)))); //add(&Bsp::s( //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( //self.tracks(), //self.scenes() //))))), //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) //)); ////if let Some(last) = self.history.last() { ////add(&FieldV(theme, format!("History ({})", self.history.len()), ////Fill::X(Align::w(format!("{:?}", last.0))))); ////} //} }