use crate::*; use std::fmt::Write; use std::path::PathBuf; use std::ffi::OsString; has!(Jack<'static>: |self: Arrangement|self.jack); has!(Measure: |self: Arrangement|self.size); #[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); #[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); #[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); #[cfg(feature = "track")] impl TracksView for Arrangement {} // -> to auto? #[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); #[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); #[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); #[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); #[cfg(feature = "select")] impl Arrangement { #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } fn selected_device (&self) -> Option { todo!() } fn unselect (&self) -> Selection { Selection::Nothing } } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } impl Arrangement { /// Create a new arrangement. pub fn new ( jack: &Jack<'static>, name: Option>, clock: Clock, tracks: Vec, scenes: Vec, midi_ins: Vec, midi_outs: Vec, ) -> Self { Self { clock, tracks, scenes, midi_ins, midi_outs, jack: jack.clone(), name: name.unwrap_or_default(), color: ItemTheme::random(), selection: Selection::TrackClip { track: 0, scene: 0 }, ..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. pub 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. pub fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } #[cfg(feature = "track")] impl Arrangement { /// Get the active track pub fn get_track (&self) -> Option<&Track> { let index = self.selection().track()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active track pub fn get_track_mut (&mut self) -> Option<&mut Track> { let index = self.selection().track()?; Has::>::get_mut(self).get_mut(index) } /// Add multiple tracks pub fn tracks_add ( &mut self, count: usize, width: Option, mins: &[Connect], mouts: &[Connect], ) -> Usually<()> { let track_color_1 = ItemColor::random(); let track_color_2 = ItemColor::random(); for i in 0..count { let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); let track = self.track_add(None, Some(color), mins, mouts)?.1; if let Some(width) = width { track.width = width; } } Ok(()) } /// Add a track pub fn track_add ( &mut self, name: Option<&str>, color: Option, mins: &[Connect], mouts: &[Connect], ) -> Usually<(usize, &mut Track)> { let name: Arc = name.map_or_else( ||format!("trk{:02}", self.track_last).into(), |x|x.to_string().into() ); self.track_last += 1; let track = Track { width: (name.len() + 2).max(12), color: color.unwrap_or_else(ItemTheme::random), sequencer: Sequencer::new( &format!("{name}"), self.jack(), Some(self.clock()), None, mins, mouts )?, name, ..Default::default() }; self.tracks_mut().push(track); let len = self.tracks().len(); let index = len - 1; for scene in self.scenes_mut().iter_mut() { while scene.clips.len() < len { scene.clips.push(None); } } Ok((index, &mut self.tracks_mut()[index])) } pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { Bsp::s( Fixed::Y(1, self.view_inputs_header()), Thunk::new(|to: &mut TuiOut|{ for (index, port) in self.midi_ins().iter().enumerate() { to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) } }) ) } fn view_inputs_header (&self) -> impl Content + '_ { Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), )))))) }))) } fn view_inputs_row (&self, port: &MidiInput) -> impl Content { Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), ))))) }))) } pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { let mut h = 1; for output in self.midi_outs().iter() { h += 1 + output.connections.len(); } let h = h as u16; let list = Bsp::s( Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&Fixed::Y(1,Fill::X(Bsp::e( Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), Fill::X(Align::e(format!("{}/{} ", port.port().get_connections().len(), port.connections.len()))))))); for (index, conn) in port.connections.iter().enumerate() { to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); } } }))))); Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), Tui::bg(theme.darker.rgb, Align::w(Fill::X( Thunk::new(|to: &mut TuiOut|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&Fixed::X(track_width(index, track), Thunk::new(|to: &mut TuiOut|{ to.place(&Fixed::Y(1, Align::w(Bsp::e( Either::new(true, Tui::fg(Green, "play "), "play "), Either::new(false, Tui::fg(Yellow, "solo "), "solo "), )))); for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&Fixed::Y(1, Align::w(Bsp::e( Either::new(true, Tui::fg(Green, " ● "), " · "), Either::new(false, Tui::fg(Yellow, " ● "), " · "), )))); for (_index, _conn) in port.connections.iter().enumerate() { to.place(&Fixed::Y(1, Fill::X(""))); } }})))}})))))) } pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { let mut h = 2u16; for track in self.tracks().iter() { h = h.max(track.devices.len() as u16 * 2); } view_track_row_section(theme, button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&Fixed::XY(track_width(index, track), h + 1, Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, |_, _index|Fixed::XY(track.width as u16, 2, Tui::fg_bg( ItemTheme::G[32].lightest.rgb, ItemTheme::G[32].dark.rgb, Align::nw(format!(" · {}", "--"))))))))); })) } } #[cfg(feature = "scene")] impl Arrangement { /// Get the active scene pub fn get_scene (&self) -> Option<&Scene> { let index = self.selection().scene()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active scene pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { let index = self.selection().scene()?; Has::>::get_mut(self).get_mut(index) } } #[cfg(feature = "scene")] impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } fn w_side (&self) -> u16 { (self.measure_width() as u16 * 2 / 10).max(20) } fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) } } #[cfg(feature = "clip")] impl Arrangement { /// Get the active clip pub fn get_clip (&self) -> Option>> { self.get_scene()?.clips.get(self.selection().track()?)?.clone() } /// Put a clip in a slot pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> ) -> Option>> { let old = self.scenes[scene].clips[track].clone(); self.scenes[scene].clips[track] = clip; old } /// Change the color of a clip, returning the previous one pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) -> Option { self.scenes[scene].clips[track].as_ref().map(|clip|{ let mut clip = clip.write().unwrap(); let old = clip.color.clone(); clip.color = color.clone(); panic!("{color:?} {old:?}"); old }) } /// Toggle looping for the active clip pub fn toggle_loop (&mut self) { if let Some(clip) = self.get_clip() { clip.write().unwrap().toggle_loop() } } } #[cfg(feature = "sampler")] impl Arrangement { /// Get the first sampler of the active track pub fn sampler (&self) -> Option<&Sampler> { self.get_track()?.sampler(0) } /// Get the first sampler of the active track pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { self.get_track_mut()?.sampler_mut(0) } } impl HasClipsSize for Arrangement { fn clips_size (&self) -> &Measure { &self.size_inner } } impl PartialEq for BrowseTarget { fn eq (&self, other: &Self) -> bool { match self { Self::ImportSample(_) => false, Self::ExportSample(_) => false, Self::ImportClip(_) => false, Self::ExportClip(_) => false, t => matches!(other, t) } } } impl Browse { pub fn new (cwd: Option) -> Usually { let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; let mut dirs = vec![]; let mut files = vec![]; for entry in std::fs::read_dir(&cwd)? { let entry = entry?; let name = entry.file_name(); let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); let meta = entry.metadata()?; if meta.is_dir() { dirs.push((name, format!("📁 {decoded}"))); } else if meta.is_file() { files.push((name, format!("📄 {decoded}"))); } } Ok(Self { cwd, dirs, files, ..Default::default() }) } pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } pub fn path (&self) -> PathBuf { self.cwd.join(if self.is_dir() { &self.dirs[self.index].0 } else if self.is_file() { &self.files[self.index - self.dirs.len()].0 } else { unreachable!() }) } pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } } impl Browse { fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } fn _todo_stub_usize (&self) -> usize { todo!() } fn _todo_stub_arc_str (&self) -> Arc { todo!() } } impl HasContent for Browse { fn content (&self) -> impl Content { Map::south(1, ||EntriesIterator { offset: 0, index: 0, length: self.dirs.len() + self.files.len(), browser: self, }, |entry, _index|Fill::X(Align::w(entry))) } } impl<'a> Iterator for EntriesIterator<'a> { type Item = Modify<&'a str>; fn next (&mut self) -> Option { let dirs = self.browser.dirs.len(); let files = self.browser.files.len(); let index = self.index; if self.index < dirs { self.index += 1; Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) } else if self.index < dirs + files { self.index += 1; Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) } else { None } } } impl MidiClip { pub fn new ( name: impl AsRef, looped: bool, length: usize, notes: Option, color: Option, ) -> Self { Self { uuid: uuid::Uuid::new_v4(), name: name.as_ref().into(), ppq: PPQ, length, notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), looped, loop_start: 0, loop_length: length, percussive: true, color: color.unwrap_or_else(ItemTheme::random) } } pub fn count_midi_messages (&self) -> usize { let mut count = 0; for tick in self.notes.iter() { count += tick.len(); } count } pub fn set_length (&mut self, length: usize) { self.length = length; self.notes = vec![Vec::with_capacity(16);length]; } pub fn duplicate (&self) -> Self { let mut clone = self.clone(); clone.uuid = uuid::Uuid::new_v4(); clone } pub fn toggle_loop (&mut self) { self.looped = !self.looped; } pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { if pulse >= self.length { panic!("extend clip first") } self.notes[pulse].push(message); } /// Check if a range `start..end` contains MIDI Note On `k` pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { for event in events.iter() { if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } } } false } pub fn stop_all () -> Self { Self::new( "Stop", false, 1, Some(vec![vec![MidiMessage::Controller { controller: 123.into(), value: 0.into() }]]), Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) ) } } impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } impl Eq for MidiClip {} impl MidiClip { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_bool_stub_ (&self) -> bool { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } } //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); impl> HasClock for T { fn clock (&self) -> &Clock { self.get() } fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } } 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 } } impl Default for ClockView { 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 ClockView { 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, "{}", ClockView::BEAT_EMPTY)); cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); cache.bpm.update(None, rewrite!(buf, "{}", ClockView::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))))); ////} //} } impl>> HasEditor for T {} has!(Measure: |self: MidiEditor|self.size); impl Default for MidiEditor { fn default () -> Self { Self { size: Measure::new(0, 0), mode: PianoHorizontal::new(None), } } } impl std::fmt::Debug for MidiEditor { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("MidiEditor").field("mode", &self.mode).finish() } } from!(MidiEditor: |clip: &Arc>| { let model = Self::from(Some(clip.clone())); model.redraw(); model }); from!(MidiEditor: |clip: Option>>| { let mut model = Self::default(); *model.clip_mut() = clip; model.redraw(); model }); impl MidiEditor { /// Put note at current position pub fn put_note (&mut self, advance: bool) { let mut redraw = false; if let Some(clip) = self.clip() { let mut clip = clip.write().unwrap(); let note_start = self.get_time_pos(); let note_pos = self.get_note_pos(); let note_len = self.get_note_len(); let note_end = note_start + (note_len.saturating_sub(1)); let key: u7 = u7::from(note_pos as u8); let vel: u7 = 100.into(); let length = clip.length; let note_end = note_end % length; let note_on = MidiMessage::NoteOn { key, vel }; if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { clip.notes[note_start].push(note_on); } let note_off = MidiMessage::NoteOff { key, vel }; if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { clip.notes[note_end].push(note_off); } if advance { self.set_time_pos((note_end + 1) % clip.length); } redraw = true; } if redraw { self.mode.redraw(); } } fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } fn note_length (&self) -> usize { self.get_note_len() } fn note_pos (&self) -> usize { self.get_note_pos() } fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } fn note_len (&self) -> usize { self.get_note_len() } fn note_len_next (&self) -> usize { self.get_note_len() + 1 } fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } fn note_range (&self) -> usize { self.get_note_axis() } fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } fn time_zoom (&self) -> usize { self.get_time_zoom() } fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } fn time_lock (&self) -> bool { self.get_time_lock() } fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } fn time_pos (&self) -> usize { self.get_time_pos() } fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } fn time_pos_prev (&self) -> usize { let step = self.get_note_len(); self.get_time_pos().overflowing_sub(step) .0.min(self.clip_length().saturating_sub(step)) } fn time_pos_prev_fine (&self) -> usize { self.get_time_pos().overflowing_sub(1) .0.min(self.clip_length().saturating_sub(1)) } pub fn clip_status (&self) -> impl Content + '_ { let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.name.clone(), clip.length, clip.looped) } else { (ItemTheme::G[64], String::new().into(), 0, false) }; Fixed::X(20, col!( Fill::X(Align::w(Bsp::e( button_2("f2", "name ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), Fill::X(Align::w(Bsp::e( button_2("l", "ength ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), Fill::X(Align::w(Bsp::e( button_2("r", "epeat ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), )) } pub fn edit_status (&self) -> impl Content + '_ { let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.length) } else { (ItemTheme::G[64], 0) }; let time_pos = self.get_time_pos(); let time_zoom = self.get_time_zoom(); let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; let note_pos = self.get_note_pos(); let note_name = format!("{:4}", note_pitch_to_name(note_pos)); let note_pos = format!("{:>3}", note_pos); let note_len = format!("{:>4}", self.get_note_len()); Fixed::X(20, col!( Fill::X(Align::w(Bsp::e( button_2("t", "ime ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} "))))))), Fill::X(Align::w(Bsp::e( button_2("z", "lock ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{time_lock}"))))))), Fill::X(Align::w(Bsp::e( button_2("x", "note ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{note_name} {note_pos} {note_len}"))))))), )) } } impl TimeRange for MidiEditor { fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } } impl NoteRange for MidiEditor { fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } } impl NotePoint for MidiEditor { fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } } impl TimePoint for MidiEditor { fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } } impl MidiViewer for MidiEditor { fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } fn redraw (&self) { self.mode.redraw() } fn clip (&self) -> &Option>> { self.mode.clip() } fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } } impl Draw for MidiEditor { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } impl Layout for MidiEditor { fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } } impl HasContent for MidiEditor { fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } } has!(Measure:|self:PianoHorizontal|self.size); impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { let size = Measure::new(0, 0); let mut range = MidiSelection::from((12, true)); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); let piano = Self { keys_width: 5, size, range, buffer: RwLock::new(Default::default()).into(), point: MidiCursor::default(), clip: clip.cloned(), color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), }; piano.redraw(); piano } } impl Draw for PianoHorizontal { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } impl Layout for PianoHorizontal { fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } } impl HasContent for PianoHorizontal { fn content (&self) -> impl Content { Bsp::s( Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), ) } } impl PianoHorizontal { /// Draw the piano roll background. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { for (y, note) in (0..=127).rev().enumerate() { for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { let cell = buf.get_mut(x, y).unwrap(); if note == (127-note_point) || time == time_point { cell.set_bg(Rgb(0,0,0)); } else { cell.set_bg(clip.color.darkest.rgb); } if time % 384 == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('│'); } else if time % 96 == 0 { cell.set_fg(clip.color.dark.rgb); cell.set_char('╎'); } else if time % note_len == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('┊'); } else if (127 - note) % 12 == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('='); } else if (127 - note) % 6 == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('—'); } else { cell.set_fg(clip.color.darker.rgb); cell.set_char('·'); } } } } /// Draw the piano roll foreground. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); let mut notes_on = [false;128]; for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { for (_y, note) in (0..=127).rev().enumerate() { if let Some(cell) = buf.get_mut(x, note) { if notes_on[note] { cell.set_char('▂'); cell.set_style(style); } } } let time_end = time_start + zoom; for time in time_start..time_end.min(clip.length) { for event in clip.notes[time].iter() { match event { MidiMessage::NoteOn { key, .. } => { let note = key.as_int() as usize; if let Some(cell) = buf.get_mut(x, note) { cell.set_char('█'); cell.set_style(style); } notes_on[note] = true }, MidiMessage::NoteOff { key, .. } => { notes_on[key.as_int() as usize] = false }, _ => {} } } } } } fn notes (&self) -> impl Content { let time_start = self.get_time_start(); let note_lo = self.get_note_lo(); let note_hi = self.get_note_hi(); let buffer = self.buffer.clone(); Thunk::new(move|to: &mut TuiOut|{ let source = buffer.read().unwrap(); let XYWH(x0, y0, w, _h) = to.area(); //if h as usize != note_axis { //panic!("area height mismatch: {h} <> {note_axis}"); //} for (area_x, screen_x) in (x0..x0+w).enumerate() { for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { let source_x = time_start + area_x; let source_y = note_hi - area_y; // TODO: enable loop rollover: //let source_x = (time_start + area_x) % source.width.max(1); //let source_y = (note_hi - area_y) % source.height.max(1); let is_in_x = source_x < source.width; let is_in_y = source_y < source.height; if is_in_x && is_in_y { if let Some(source_cell) = source.get(source_x, source_y) { if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { *cell = source_cell.clone(); } } } } } }) } fn cursor (&self) -> impl Content { let note_hi = self.get_note_hi(); let note_lo = self.get_note_lo(); let note_pos = self.get_note_pos(); let note_len = self.get_note_len(); let time_pos = self.get_time_pos(); let time_start = self.get_time_start(); let time_zoom = self.get_time_zoom(); let style = Some(Style::default().fg(self.color.lightest.rgb)); Thunk::new(move|to: &mut TuiOut|{ let XYWH(x0, y0, w, _) = to.area(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { if note == note_pos { for x in 0..w { let screen_x = x0 + x; let time_1 = time_start + x as usize * time_zoom; let time_2 = time_1 + time_zoom; if time_1 <= time_pos && time_pos < time_2 { to.blit(&"█", screen_x, screen_y, style); let tail = note_len as u16 / time_zoom as u16; for x_tail in (screen_x + 1)..(screen_x + tail) { to.blit(&"▂", x_tail, screen_y, style); } break } } break } } }) } fn keys (&self) -> impl Content { let state = self; let color = state.color; let note_lo = state.get_note_lo(); let note_hi = state.get_note_hi(); let note_pos = state.get_note_pos(); let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(Tui::g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ let XYWH(x, y0, _w, _h) = to.area(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { to.blit(&to_key(note), x, screen_y, key_style); if note > 127 { continue } if note == note_pos { to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) } else { to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) }; } }))) } fn timeline (&self) -> impl Content + '_ { Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ let XYWH(x, y, w, _h) = to.area(); let style = Some(Style::default().dim()); let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { let t = area_x as usize * self.time_zoom().get(); if t < length { to.blit(&"|", screen_x, y, style); } } }))) } } impl TimeRange for PianoHorizontal { fn time_len (&self) -> &AtomicUsize { self.range.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } fn time_start (&self) -> &AtomicUsize { self.range.time_start() } fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } } impl NoteRange for PianoHorizontal { fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } } impl NotePoint for PianoHorizontal { fn note_len (&self) -> &AtomicUsize { self.point.note_len() } fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } } impl TimePoint for PianoHorizontal { fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } } impl MidiViewer for PianoHorizontal { fn clip (&self) -> &Option>> { &self.clip } fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } /// Determine the required space to render the clip. fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } fn redraw (&self) { *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { let clip = clip.read().unwrap(); let buf_size = self.buffer_size(&clip); let mut buffer = BigBuffer::from(buf_size); let time_zoom = self.get_time_zoom(); self.time_len().set(clip.length); PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); buffer } else { Default::default() } } fn set_clip (&mut self, clip: Option<&Arc>>) { *self.clip_mut() = clip.cloned(); self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); self.redraw(); } } impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { let buffer = self.buffer.read().unwrap(); f.debug_struct("PianoHorizontal") .field("time_zoom", &self.range.time_zoom) .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) .finish() } } impl Default for OctaveVertical { fn default () -> Self { Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } } } impl OctaveVertical { fn color (&self, pitch: usize) -> Color { let pitch = pitch % 12; self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] } } impl HasContent for OctaveVertical { fn content (&self) -> impl Content + '_ { row!( Tui::fg_bg(self.color(0), self.color(1), "▙"), Tui::fg_bg(self.color(2), self.color(3), "▙"), Tui::fg_bg(self.color(4), self.color(5), "▌"), Tui::fg_bg(self.color(6), self.color(7), "▟"), Tui::fg_bg(self.color(8), self.color(9), "▟"), Tui::fg_bg(self.color(10), self.color(11), "▟"), ) } } impl Lv2 { pub fn new ( jack: &Jack<'static>, name: &str, uri: &str, ) -> Usually { let lv2_world = livi::World::with_load_bundle(&uri); let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { min_block_length: 1, max_block_length: 65536, }); let lv2_plugin = lv2_world.iter_plugins().nth(0) .unwrap_or_else(||panic!("plugin not found: {uri}")); Ok(Self { jack: jack.clone(), name: name.into(), path: Some(String::from(uri).into()), selected: 0, mapping: false, midi_ins: vec![], midi_outs: vec![], audio_ins: vec![], audio_outs: vec![], lv2_instance: unsafe { lv2_plugin .instantiate(lv2_features.clone(), 48000.0) .expect(&format!("instantiate failed: {uri}")) }, lv2_port_list: lv2_plugin.ports().collect::>(), lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), lv2_ui_thread: None, lv2_world, lv2_features, lv2_plugin, }) } const INPUT_BUFFER: usize = 1024; } audio!(|self: Lv2, _client, scope|{ let Self { midi_ins, midi_outs, audio_ins, audio_outs, lv2_features, lv2_instance, lv2_input_buffer, .. } = self; let urid = lv2_features.midi_urid(); lv2_input_buffer.clear(); for port in midi_ins.iter() { let mut atom = ::livi::event::LV2AtomSequence::new( &lv2_features, scope.n_frames() as usize ); for event in port.iter(scope) { match event.bytes.len() { 3 => atom.push_midi_event::<3>( event.time as i64, urid, &event.bytes[0..3] ).unwrap(), _ => {} } } lv2_input_buffer.push(atom); } let mut outputs = vec![]; for _ in midi_outs.iter() { outputs.push(::livi::event::LV2AtomSequence::new( lv2_features, scope.n_frames() as usize )); } let ports = ::livi::EmptyPortConnections::new() .with_atom_sequence_inputs(lv2_input_buffer.iter()) .with_atom_sequence_outputs(outputs.iter_mut()) .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); unsafe { lv2_instance.run(scope.n_frames() as usize, ports).unwrap() }; Control::Continue }); impl Draw for Lv2 { fn draw (&self, to: &mut TuiOut) { let area = to.area(); let XYWH(x, y, _, height) = area; let mut width = 20u16; let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); let end = start + height as usize - 2; //draw_box(buf, Rect { x, y, width, height }); for i in start..end { if let Some(port) = self.lv2_port_list.get(i) { let value = if let Some(value) = self.lv2_instance.control_input(port.index) { value } else { port.default_value }; //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); let label = &format!("{:25} = {value:.03}", port.name); width = width.max(label.len() as u16 + 4); let style = if i == self.selected { Some(Style::default().green()) } else { None } ; to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); } else { break } } draw_header(self, to, x, y, width); } } #[cfg(feature = "lv2_gui")] impl LV2PluginUI { pub fn new () -> Usually { Ok(Self { window: None }) } } #[cfg(feature = "lv2_gui")] impl ApplicationHandler for LV2PluginUI { fn resumed (&mut self, event_loop: &ActiveEventLoop) { self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); } fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => { self.window.as_ref().unwrap().set_visible(false); event_loop.exit(); }, WindowEvent::RedrawRequested => { self.window.as_ref().unwrap().request_redraw(); } _ => (), } } } impl Layout for RmsMeter {} impl Draw for RmsMeter { fn draw (&self, to: &mut TuiOut) { let XYWH(x, y, w, h) = to.area(); let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32).ceil() as u16; let y2 = y + h; //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); for y in y..(y + v) { for x in x..(x + w) { to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); } } } } impl Layout for Log10Meter {} impl Draw for Log10Meter { fn draw (&self, to: &mut TuiOut) { let XYWH(x, y, w, h) = to.area(); let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32 / 100.0).ceil() as u16; let y2 = y + h; //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); for y in y..(y + v) { for x in x..(x + w) { to.blit(&"▌", x, y2 - y, Some(Style::default().green())); } } } } impl Default for Pool { fn default () -> Self { //use PoolMode::*; Self { clip: 0.into(), mode: None, visible: true, #[cfg(feature = "clip")] clips: Arc::from(RwLock::from(vec![])), #[cfg(feature = "sampler")] samples: Arc::from(RwLock::from(vec![])), #[cfg(feature = "browse")] browse: None, } } } impl Pool { pub fn clip_index (&self) -> usize { self.clip.load(Relaxed) } pub fn set_clip_index (&self, value: usize) { self.clip.store(value, Relaxed); } pub fn mode (&self) -> &Option { &self.mode } pub fn mode_mut (&mut self) -> &mut Option { &mut self.mode } pub fn begin_clip_length (&mut self) { let length = self.clips()[self.clip_index()].read().unwrap().length; *self.mode_mut() = Some(PoolMode::Length( self.clip_index(), length, ClipLengthFocus::Bar )); } pub fn begin_clip_rename (&mut self) { let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); *self.mode_mut() = Some(PoolMode::Rename( self.clip_index(), name )); } pub fn begin_import (&mut self) -> Usually<()> { *self.mode_mut() = Some(PoolMode::Import( self.clip_index(), Browse::new(None)? )); Ok(()) } pub fn begin_export (&mut self) -> Usually<()> { *self.mode_mut() = Some(PoolMode::Export( self.clip_index(), Browse::new(None)? )); Ok(()) } pub fn new_clip (&self) -> MidiClip { MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) } pub fn cloned_clip (&self) -> MidiClip { let index = self.clip_index(); let mut clip = self.clips()[index].read().unwrap().duplicate(); clip.color = ItemTheme::random_near(clip.color, 0.25); clip } pub fn add_new_clip (&self) -> (usize, Arc>) { let clip = Arc::new(RwLock::new(self.new_clip())); let index = { let mut clips = self.clips.write().unwrap(); clips.push(clip.clone()); clips.len().saturating_sub(1) }; self.clip.store(index, Relaxed); (index, clip) } pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); if let Some(index) = index { self.clips.write().unwrap().remove(index); return true } false } } impl ClipLengthFocus { pub fn next (&mut self) { use ClipLengthFocus::*; *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } } pub fn prev (&mut self) { use ClipLengthFocus::*; *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } } } impl ClipLength { pub fn _new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } } pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) } pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq } pub fn ticks (&self) -> usize { self.pulses % self.ppq } pub fn bars_string (&self) -> Arc { format!("{}", self.bars()).into() } pub fn beats_string (&self) -> Arc { format!("{}", self.beats()).into() } pub fn ticks_string (&self) -> Arc { format!("{:>02}", self.ticks()).into() } } #[macro_export] macro_rules! has_clips { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { $cb.read().unwrap() } fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { $cb.write().unwrap() } } } } has_clips!(|self: Pool|self.clips); has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); from!(Pool: |clip:&Arc>|{ let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); model }); impl Pool { fn _todo_usize_ (&self) -> usize { todo!() } fn _todo_bool_ (&self) -> bool { todo!() } fn _todo_clip_ (&self) -> MidiClip { todo!() } fn _todo_path_ (&self) -> PathBuf { todo!() } fn _todo_color_ (&self) -> ItemColor { todo!() } fn _todo_str_ (&self) -> Arc { todo!() } fn clip_new (&self) -> MidiClip { self.new_clip() } fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } fn clip_index_current (&self) -> usize { 0 } fn clip_index_after (&self) -> usize { 0 } fn clip_index_previous (&self) -> usize { 0 } fn clip_index_next (&self) -> usize { 0 } fn color_random (&self) -> ItemColor { ItemColor::random() } } impl<'a> HasContent for PoolView<'a> { fn content (&self) -> impl Content { let Self(pool) = self; //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); //let height = pool.clips.read().unwrap().len() as u16; Fixed::X(20, Fill::Y(Align::n(Map::new( ||pool.clips().clone().into_iter(), move|clip: Arc>, i: usize|{ let item_height = 1; let item_offset = i as u16 * item_height; let selected = i == pool.clip_index(); let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let bg = if selected { color.light.rgb } else { color.base.rgb }; let fg = color.lightest.rgb; let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if false { String::default() } else { format!("{length} ") }; Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), )))) })))) } } impl HasContent for ClipLength { fn content (&self) -> impl Content + '_ { use ClipLengthFocus::*; let bars = ||self.bars_string(); let beats = ||self.beats_string(); let ticks = ||self.ticks_string(); match self.focus { None => row!(" ", bars(), ".", beats(), ".", ticks()), Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), } } } impl> RegisterPorts for J { fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { MidiInput::new(self.jack(), name, connect) } fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { MidiOutput::new(self.jack(), name, connect) } fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { AudioInput::new(self.jack(), name, connect) } fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { AudioOutput::new(self.jack(), name, connect) } } //take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); //take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); impl Connect { pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) -> Vec { let mut connections = vec![]; for port in exact.iter() { connections.push(Self::exact(port)) } for port in re.iter() { connections.push(Self::regexp(port)) } for port in re_all.iter() { connections.push(Self::regexp_all(port)) } connections } /// Connect to this exact port pub fn exact (name: impl AsRef) -> Self { let info = format!("=:{}", name.as_ref()).into(); let name = Some(Exact(name.as_ref().into())); Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } } pub fn regexp (name: impl AsRef) -> Self { let info = format!("~:{}", name.as_ref()).into(); let name = Some(RegExp(name.as_ref().into())); Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } } pub fn regexp_all (name: impl AsRef) -> Self { let info = format!("+:{}", name.as_ref()).into(); let name = Some(RegExp(name.as_ref().into())); Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } } pub fn info (&self) -> Arc { format!(" ({}) {} {}", { let status = self.status.read().unwrap(); let mut ok = 0; for (_, _, state) in status.iter() { if *state == Connected { ok += 1 } } format!("{ok}/{}", status.len()) }, match self.scope { None => "x", Some(One) => " ", Some(All) => "*", }, match &self.name { None => format!("x"), Some(Exact(name)) => format!("= {name}"), Some(RegExp(name)) => format!("~ {name}"), }).into() } } impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for MidiInput { type Port = MidiIn; type Pair = MidiOut; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self { port: Self::register(jack, name)?, jack: jack.clone(), name: name.as_ref().into(), connections: connect.to_vec(), held: Arc::new(RwLock::new([false;128])) }; port.connect_to_matching()?; Ok(port) } } impl MidiInput { pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { parse_midi_input(self.port().iter(scope)) } } impl>> HasMidiIns for T { fn midi_ins (&self) -> &Vec { self.get() } fn midi_ins_mut (&mut self) -> &mut Vec { self.get_mut() } } impl> AddMidiIn for T { fn midi_in_add (&mut self) -> Usually<()> { let index = self.midi_ins().len(); let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; self.midi_ins_mut().push(port); Ok(()) } } impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for MidiOutput { type Port = MidiOut; type Pair = MidiIn; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self::register(jack, name)?; let jack = jack.clone(); let name = name.as_ref().into(); let connections = connect.to_vec(); let port = Self { jack, port, name, connections, held: Arc::new([false;128].into()), note_buffer: vec![0;8], output_buffer: vec![vec![];65536], }; port.connect_to_matching()?; Ok(port) } } impl MidiOutput { /// Clear the section of the output buffer that we will be using, /// emitting "all notes off" at start of buffer if requested. pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); for frame in &mut self.output_buffer[0..n_frames] { frame.clear(); } if reset { all_notes_off(&mut self.output_buffer); } } /// Write a note to the output buffer pub fn buffer_write <'a> ( &'a mut self, sample: usize, event: LiveEvent, ) { self.note_buffer.fill(0); event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); self.output_buffer[sample].push(self.note_buffer.clone()); // Update the list of currently held notes. if let LiveEvent::Midi { ref message, .. } = event { update_keys(&mut*self.held.write().unwrap(), message); } } /// Write a chunk of MIDI data from the output buffer to the output port. pub fn buffer_emit (&mut self, scope: &ProcessScope) { let samples = scope.n_frames() as usize; let mut writer = self.port.writer(scope); for (time, events) in self.output_buffer.iter().enumerate().take(samples) { for bytes in events.iter() { writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ panic!("Failed to write MIDI data: {bytes:?}"); }); } } } } impl>> HasMidiOuts for T { fn midi_outs (&self) -> &Vec { self.get() } fn midi_outs_mut (&mut self) -> &mut Vec { self.get_mut() } } /// Trail for thing that may gain new MIDI ports. impl> AddMidiOut for T { fn midi_out_add (&mut self) -> Usually<()> { let index = self.midi_outs().len(); let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; self.midi_outs_mut().push(port); Ok(()) } } impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for AudioInput { type Port = AudioIn; type Pair = AudioOut; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self { port: Self::register(jack, name)?, jack: jack.clone(), name: name.as_ref().into(), connections: connect.to_vec() }; port.connect_to_matching()?; Ok(port) } } impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for AudioOutput { type Port = AudioOut; type Pair = AudioIn; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self { port: Self::register(jack, name)?, jack: jack.clone(), name: name.as_ref().into(), connections: connect.to_vec() }; port.connect_to_matching()?; Ok(port) } } impl Draw for AddSampleModal { fn draw (&self, _to: &mut TuiOut) { todo!() //let area = to.area(); //to.make_dim(); //let area = center_box( //area, //64.max(area.w().saturating_sub(8)), //20.max(area.w().saturating_sub(8)), //); //to.fill_fg(area, Color::Reset); //to.fill_bg(area, Nord::bg_lo(true, true)); //to.fill_char(area, ' '); //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; //for (i, (is_dir, name)) in self.subdirs.iter() //.map(|path|(true, path)) //.chain(self.files.iter().map(|path|(false, path))) //.enumerate() //.skip(self.offset) //{ //if i >= area.h() as usize - 4 { //break //} //let t = if is_dir { "" } else { "" }; //let line = format!("{t} {}", name.to_string_lossy()); //let line = &line[..line.len().min(area.w() as usize - 4)]; //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { //Style::default().green() //} else { //Style::default().white() //}))?; //} //Lozenge(Style::default()).draw(to) } } impl Sampler { pub fn view_grid (&self) -> impl Content + use<'_> { //let cells_x = 8u16; //let cells_y = 8u16; //let cell_width = 10u16; //let cell_height = 2u16; //let width = cells_x * cell_width; //let height = cells_y * cell_height; //let cols = Map::east( //cell_width, //move||0..cells_x, //move|x, _|Map::south( //cell_height, //move||0..cells_y, //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) //) //); //cols //Thunk::new(|to: &mut TuiOut|{ //}) "TODO" } pub fn view_grid_cell <'a> ( &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 ) -> impl Content + use<'a> { let cursor = self.cursor(); let hi_fg = Color::Rgb(64, 64, 64); let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; let tx_fg = if let Some((index, _)) = self.recording && index % 8 == x as usize && index / 8 == y as usize { Color::Rgb(255, 64, 0) } else { Color::Rgb(255, 255, 255) }; let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { Color::Rgb(96, 96, 96) } else { Color::Rgb(64, 64, 64) }; let lo_fg = Color::Rgb(64, 64, 64); let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; Fixed::XY(w, h, Bsp::s( Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, Repeat::X(Phat::<()>::LO))), Bsp::n( Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, Repeat::X(Phat::<()>::HI))), Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), ), )) } const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; pub fn view_list <'a, T: NotePoint + NoteRange> ( &'a self, compact: bool, editor: &T ) -> impl Content + 'a { let note_lo = editor.get_note_lo(); let note_pt = editor.get_note_pos(); let note_hi = editor.get_note_hi(); Fixed::X(if compact { 4 } else { 12 }, Map::south( 1, move||(note_lo..=note_hi).rev(), move|note, _index| { //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; let mut fg = Tui::g(160); if let Some(mapped) = &self.mapped[note] { let sample = mapped.read().unwrap(); fg = if note == note_pt { sample.color.lightest.rgb } else { Tui::g(224) }; bg = if note == note_pt { sample.color.light.rgb } else { sample.color.base.rgb }; } if let Some((index, _)) = self.recording { if note == index { bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; fg = Color::Rgb(224,64,32) } } Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) })) } pub fn view_list_item (&self, note: usize, compact: bool) -> String { if compact { String::default() } else { draw_list_item(&self.mapped[note]) } } pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { Outer(true, Style::default().fg(Tui::g(96))) .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None }))) } pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None }))) } pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None })) } pub fn view_status (&self, index: usize) -> impl Content { draw_status(self.mapped[index].as_ref()) } pub fn view_meters_input (&self) -> impl Content + use<'_> { draw_meters(&self.input_meters) } pub fn view_meters_output (&self) -> impl Content + use<'_> { draw_meters(&self.output_meters) } } fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ Fill::Y(RmsMeter(*value)) }))) } fn draw_list_item (sample: &Option>>) -> String { if let Some(sample) = sample { let sample = sample.read().unwrap(); format!("{:8}", sample.name) //format!("{:8} {:3} {:6}-{:6}/{:6}", //sample.name, //sample.gain, //sample.start, //sample.end, //sample.channels[0].len() //) } else { String::from("........") } } fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { let min_db = -64.0; Thunk::new(move|to: &mut TuiOut|{ let XYWH(x, y, width, height) = to.area(); let area = Rect { x, y, width, height }; if let Some(sample) = &sample { let sample = sample.read().unwrap(); let start = sample.start as f64; let end = sample.end as f64; let length = end - start; let step = length / width as f64; let mut t = start; let mut lines = vec![]; while t < end { let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; let total: f32 = chunk.iter().map(|x|x.abs()).sum(); let count = chunk.len() as f32; let meter = 10. * (total / count).log10(); let x = t as f64; let y = meter as f64; lines.push(Line::new(x, min_db, x, y, Color::Green)); t += step / 2.; } Canvas::default() .x_bounds([sample.start as f64, sample.end as f64]) .y_bounds([min_db, 0.]) .paint(|ctx| { for line in lines.iter() { ctx.draw(line); } //FIXME: proportions //let text = "press record to finish sampling"; //ctx.print( //(width - text.len() as u16) as f64 / 2.0, //height as f64 / 2.0, //text.red() //); }).render(area, &mut to.buffer); } else { Canvas::default() .x_bounds([0.0, width as f64]) .y_bounds([0.0, height as f64]) .paint(|_ctx| { //let text = "press record to begin sampling"; //ctx.print( //(width - text.len() as u16) as f64 / 2.0, //height as f64 / 2.0, //text.red() //); }) .render(area, &mut to.buffer); } }) } impl Sampler { fn sample_selected (&self) -> usize { (self.get_note_pos() as u8).into() } fn sample_selected_pitch (&self) -> u7 { (self.get_note_pos() as u8).into() } } impl AddSampleModal { fn exited (&self) -> bool { self.exited } fn exit (&mut self) { self.exited = true } } impl AddSampleModal { pub fn new ( sample: &Arc>, voices: &Arc>> ) -> Usually { let dir = std::env::current_dir()?; let (subdirs, files) = scan(&dir)?; Ok(Self { exited: false, dir, subdirs, files, cursor: 0, offset: 0, sample: sample.clone(), voices: voices.clone(), _search: None }) } fn rescan (&mut self) -> Usually<()> { scan(&self.dir).map(|(subdirs, files)|{ self.subdirs = subdirs; self.files = files; }) } fn prev (&mut self) { self.cursor = self.cursor.saturating_sub(1); } fn next (&mut self) { self.cursor = self.cursor + 1; } fn try_preview (&mut self) -> Usually<()> { if let Some(path) = self.cursor_file() { if let Ok(sample) = Sample::from_file(&path) { *self.sample.write().unwrap() = sample; self.voices.write().unwrap().push( Sample::play(&self.sample, 0, &u7::from(100u8)) ); } //load_sample(&path)?; //let src = std::fs::File::open(&path)?; //let mss = MediaSourceStream::new(Box::new(src), Default::default()); //let mut hint = Hint::new(); //if let Some(ext) = path.extension() { //hint.with_extension(&ext.to_string_lossy()); //} //let meta_opts: MetadataOptions = Default::default(); //let fmt_opts: FormatOptions = Default::default(); //if let Ok(mut probed) = symphonia::default::get_probe() //.format(&hint, mss, &fmt_opts, &meta_opts) //{ //panic!("{:?}", probed.format.metadata()); //}; } Ok(()) } fn cursor_dir (&self) -> Option { if self.cursor < self.subdirs.len() { Some(self.dir.join(&self.subdirs[self.cursor])) } else { None } } fn cursor_file (&self) -> Option { if self.cursor < self.subdirs.len() { return None } let index = self.cursor.saturating_sub(self.subdirs.len()); if index < self.files.len() { Some(self.dir.join(&self.files[index])) } else { None } } fn pick (&mut self) -> Usually { if self.cursor == 0 { if let Some(parent) = self.dir.parent() { self.dir = parent.into(); self.rescan()?; self.cursor = 0; return Ok(false) } } if let Some(dir) = self.cursor_dir() { self.dir = dir; self.rescan()?; self.cursor = 0; return Ok(false) } if let Some(path) = self.cursor_file() { let (end, channels) = read_sample_data(&path.to_string_lossy())?; let mut sample = self.sample.write().unwrap(); sample.name = path.file_name().unwrap().to_string_lossy().into(); sample.end = end; sample.channels = channels; return Ok(true) } return Ok(false) } } impl Sampler { pub fn process_audio_in (&mut self, scope: &ProcessScope) { self.reset_input_meters(); if self.recording.is_some() { self.record_into(scope); } else { self.update_input_meters(scope); } } /// Make sure that input meter count corresponds to input channel count fn reset_input_meters (&mut self) { let channels = self.audio_ins.len(); if self.input_meters.len() != channels { self.input_meters = vec![f32::MIN;channels]; } } /// Record from inputs to sample fn record_into (&mut self, scope: &ProcessScope) { if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { let mut sample = sample.write().unwrap(); if sample.channels.len() != self.audio_ins.len() { panic!("channel count mismatch"); } let samples_with_meters = self.audio_ins.iter() .zip(self.input_meters.iter_mut()) .zip(sample.channels.iter_mut()); let mut length = 0; for ((input, meter), channel) in samples_with_meters { let slice = input.port().as_slice(scope); length = length.max(slice.len()); *meter = to_rms(slice); channel.extend_from_slice(slice); } sample.end += length; } else { panic!("tried to record into the void") } } /// Update input meters fn update_input_meters (&mut self, scope: &ProcessScope) { for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { let slice = input.port().as_slice(scope); *meter = to_rms(slice); } } /// Make sure that output meter count corresponds to input channel count fn reset_output_meters (&mut self) { let channels = self.audio_outs.len(); if self.output_meters.len() != channels { self.output_meters = vec![f32::MIN;channels]; } } /// Mix all currently playing samples into the output. pub fn process_audio_out (&mut self, scope: &ProcessScope) { self.clear_output_buffer(); self.populate_output_buffer(scope.n_frames() as usize); self.write_output_buffer(scope); } /// Zero the output buffer. fn clear_output_buffer (&mut self) { for buffer in self.buffer.iter_mut() { buffer.fill(0.0); } } /// Write playing voices to output buffer fn populate_output_buffer (&mut self, frames: usize) { let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; let channel_count = buffer.len(); match mixing_mode { MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) }), MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) }), } } /// Write output buffer to output ports. fn write_output_buffer (&mut self, scope: &ProcessScope) { let Sampler { audio_outs, buffer, .. } = self; for (i, port) in audio_outs.iter_mut().enumerate() { let buffer = &buffer[i]; for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { *value = *buffer.get(i).unwrap_or(&0.0); } } } } impl Iterator for Voice { type Item = [f32;2]; fn next (&mut self) -> Option { if self.after > 0 { self.after -= 1; return Some([0.0, 0.0]) } let sample = self.sample.read().unwrap(); if self.position < sample.end { let position = self.position; self.position += 1; return sample.channels[0].get(position).map(|_amplitude|[ sample.channels[0][position] * self.velocity * sample.gain, sample.channels[0][position] * self.velocity * sample.gain, ]) } None } } impl Sampler { pub fn new ( jack: &Jack<'static>, name: impl AsRef, #[cfg(feature = "port")] midi_from: &[Connect], #[cfg(feature = "port")] audio_from: &[&[Connect];2], #[cfg(feature = "port")] audio_to: &[&[Connect];2], ) -> Usually { let name = name.as_ref(); Ok(Self { name: name.into(), #[cfg(feature = "port")] midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, #[cfg(feature = "port")] audio_ins: vec![ AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, ], #[cfg(feature = "port")] audio_outs: vec![ AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, ], input_meters: vec![0.0;2], output_meters: vec![0.0;2], mapped: [const { None };128], unmapped: vec![], voices: Arc::new(RwLock::new(vec![])), buffer: vec![vec![0.0;16384];2], output_gain: 1., recording: None, mode: None, editing: None, size: Default::default(), note_lo: 0.into(), note_pt: 0.into(), cursor: (0.into(), 0.into()), color: Default::default(), mixing_mode: Default::default(), metering_mode: Default::default(), }) } /// Value of cursor pub fn cursor (&self) -> (usize, usize) { (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) } } impl NoteRange for Sampler { fn note_lo (&self) -> &AtomicUsize { &self.note_lo } fn note_axis (&self) -> &AtomicUsize { &self.size.y } } impl NotePoint for Sampler { fn note_len (&self) -> &AtomicUsize { unreachable!(); } fn get_note_len (&self) -> usize { 0 } fn set_note_len (&self, x: usize) -> usize { 0 /*TODO?*/ } fn note_pos (&self) -> &AtomicUsize { &self.note_pt } fn get_note_pos (&self) -> usize { self.note_pt.load(Relaxed) } fn set_note_pos (&self, x: usize) -> usize { let old = self.note_pt.swap(x, Relaxed); self.cursor.0.store(x % 8, Relaxed); self.cursor.1.store(x / 8, Relaxed); old } } impl Sample { pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0, color: ItemTheme::random(), } } pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { Voice { sample: sample.clone(), after, position: sample.read().unwrap().start, velocity: velocity.as_int() as f32 / 127.0, } } } impl Sample { /// Read WAV from file pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { let mut channels: Vec> = vec![]; for channel in wavers::Wav::from_path(src)?.channels() { channels.push(channel); } let mut end = 0; let mut data: Vec> = vec![]; for samples in channels.iter() { let channel = Vec::from(samples.as_ref()); end = end.max(channel.len()); data.push(channel); } Ok((end, data)) } pub fn from_file (path: &PathBuf) -> Usually { let name = path.file_name().unwrap().to_string_lossy().into(); let mut sample = Self { name, ..Default::default() }; // Use file extension if present let mut hint = Hint::new(); if let Some(ext) = path.extension() { hint.with_extension(&ext.to_string_lossy()); } let probed = symphonia::default::get_probe().format( &hint, MediaSourceStream::new( Box::new(File::open(path)?), Default::default(), ), &Default::default(), &Default::default() )?; let mut format = probed.format; let params = &format.tracks().iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .expect("no tracks found") .codec_params; let mut decoder = get_codecs().make(params, &Default::default())?; loop { match format.next_packet() { Ok(packet) => sample.decode_packet(&mut decoder, packet)?, Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), Err(err) => return Err(err.into()), }; }; sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); Ok(sample) } fn decode_packet ( &mut self, decoder: &mut Box, packet: Packet ) -> Usually<()> { // Decode a packet let decoded = decoder .decode(&packet) .map_err(|e|Box::::from(e))?; // Determine sample rate let spec = *decoded.spec(); if let Some(rate) = self.rate { if rate != spec.rate as usize { panic!("sample rate changed"); } } else { self.rate = Some(spec.rate as usize); } // Determine channel count while self.channels.len() < spec.channels.count() { self.channels.push(vec![]); } // Load sample let mut samples = SampleBuffer::new( decoded.frames() as u64, spec ); if samples.capacity() > 0 { samples.copy_interleaved_ref(decoded); for frame in samples.samples().chunks(spec.channels.count()) { for (chan, frame) in frame.iter().enumerate() { self.channels[chan].push(*frame) } } } Ok(()) } } impl Sampler { /// Create [Voice]s from [Sample]s in response to MIDI input. pub fn process_midi_in (&mut self, scope: &ProcessScope) { let Sampler { midi_in, mapped, voices, .. } = self; for RawMidi { time, bytes } in midi_in.port().iter(scope) { if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { match message { MidiMessage::NoteOn { ref key, ref vel } => { if let Some(ref sample) = mapped[key.as_int() as usize] { voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); } }, MidiMessage::Controller { controller: _, value: _ } => { // TODO } _ => {} } } } } } impl Sample { pub fn handle_cc (&mut self, controller: u7, value: u7) { let percentage = value.as_int() as f64 / 127.; match controller.as_int() { 20 => { self.start = (percentage * self.end as f64) as usize; }, 21 => { let length = self.channels[0].len(); self.end = length.min( self.start + (percentage * (length as f64 - self.start as f64)) as usize ); }, 22 => { /*attack*/ }, 23 => { /*decay*/ }, 24 => { self.gain = percentage as f32 * 2.0; }, 26 => { /* pan */ } 25 => { /* pitch */ } _ => {} } } } audio!(|self: Sampler, _client, scope|{ self.process_midi_in(scope); self.process_audio_out(scope); self.process_audio_in(scope); Control::Continue }); impl> + Send + Sync> HasScenes for T {} impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } impl AddScene for T {} impl Scene { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } } impl> + Send + Sync> HasScene for T {} impl Scene { /// Returns the pulse length of the longest clip in the scene pub fn pulses (&self) -> usize { self.clips.iter().fold(0, |a, p|{ a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) }) } /// Returns true if all clips in the scene are /// currently playing on the given collection of tracks. pub fn is_playing (&self, tracks: &[Track]) -> bool { self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() .all(|(track_index, clip)|match clip { Some(c) => tracks .get(track_index) .map(|track|{ if let Some((_, Some(clip))) = track.sequencer().play_clip() { *clip.read().unwrap() == *c.read().unwrap() } else { false } }) .unwrap_or(false), None => true }) } pub fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } } impl> HasSelection for T {} #[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!() } } } #[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!() } } } 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.clip(*track) { Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), None => format!("T{track} S{scene}: Empty") }, _ => format!("T{track} S{scene}: Empty"), }, _ => todo!() }).into() } } impl> HasSequencer for T { fn sequencer (&self) -> &Sequencer { self.get() } fn sequencer_mut (&mut self) -> &mut Sequencer { self.get_mut() } } impl Default for Sequencer { fn default () -> Self { Self { #[cfg(feature = "clock")] clock: Clock::default(), #[cfg(feature = "clip")] play_clip: None, #[cfg(feature = "clip")] next_clip: None, #[cfg(feature = "port")] midi_ins: vec![], #[cfg(feature = "port")] midi_outs: vec![], recording: false, monitoring: true, overdub: false, notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), note_buf: vec![0;8], midi_buf: vec![], reset: true, } } } impl Sequencer { pub fn new ( name: impl AsRef, jack: &Jack<'static>, #[cfg(feature = "clock")] clock: Option<&Clock>, #[cfg(feature = "clip")] clip: Option<&Arc>>, #[cfg(feature = "port")] midi_from: &[Connect], #[cfg(feature = "port")] midi_to: &[Connect], ) -> Usually { let _name = name.as_ref(); #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); Ok(Self { reset: true, notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), #[cfg(feature = "clock")] clock, ..Default::default() }) } } impl std::fmt::Debug for Sequencer { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Sequencer") .field("clock", &self.clock) .field("play_clip", &self.play_clip) .field("next_clip", &self.next_clip) .finish() } } #[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); #[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); #[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); impl MidiMonitor for Sequencer { fn notes_in (&self) -> &Arc> { &self.notes_in } fn monitoring (&self) -> bool { self.monitoring } fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } } impl MidiRecord for Sequencer { fn recording (&self) -> bool { self.recording } fn recording_mut (&mut self) -> &mut bool { &mut self.recording } fn overdub (&self) -> bool { self.overdub } fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } } #[cfg(feature="clip")] impl HasPlayClip for Sequencer { fn reset (&self) -> bool { self.reset } fn reset_mut (&mut self) -> &mut bool { &mut self.reset } fn play_clip (&self) -> &Option<(Moment, Option>>)> { &self.play_clip } fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { &mut self.play_clip } fn next_clip (&self) -> &Option<(Moment, Option>>)> { &self.next_clip } fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { &mut self.next_clip } } /// JACK process callback for a sequencer's clip sequencer/recorder. impl Audio for Sequencer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { if self.clock().is_rolling() { self.process_rolling(scope) } else { self.process_stopped(scope) } } } impl Sequencer { fn process_rolling (&mut self, scope: &ProcessScope) -> Control { self.process_clear(scope, false); // Write chunk of clip to output, handle switchover if self.process_playback(scope) { self.process_switchover(scope); } // Monitor input to output self.process_monitoring(scope); // Record and/or monitor input self.process_recording(scope); // Emit contents of MIDI buffers to JACK MIDI output ports. self.midi_outs_emit(scope); Control::Continue } fn process_stopped (&mut self, scope: &ProcessScope) -> Control { if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { self.process_monitoring(scope) } Control::Continue } fn process_monitoring (&mut self, scope: &ProcessScope) { let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat let monitoring = self.monitoring(); for input in self.midi_ins.iter() { for (sample, event, bytes) in input.parsed(scope) { if let LiveEvent::Midi { message, .. } = event { if monitoring { self.midi_buf[sample].push(bytes.to_vec()); } // FIXME: don't lock on every event! update_keys(&mut notes_in.write().unwrap(), &message); } } } } /// Clear the section of the output buffer that we will be using, /// emitting "all notes off" at start of buffer if requested. fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); for frame in &mut self.midi_buf_mut()[0..n_frames] { frame.clear(); } if reset { all_notes_off(self.midi_buf_mut()); } for port in self.midi_outs_mut().iter_mut() { // Clear output buffer(s) port.buffer_clear(scope, false); } } fn process_recording (&mut self, scope: &ProcessScope) { if self.monitoring() { self.monitor(scope); } if let Some((started, ref clip)) = self.play_clip.clone() { self.record_clip(scope, started, clip); } if let Some((_start_at, _clip)) = &self.next_clip() { self.record_next(); } } fn process_playback (&mut self, scope: &ProcessScope) -> bool { // If a clip is playing, write a chunk of MIDI events from it to the output buffer. // If no clip is playing, prepare for switchover immediately. if let Some((started, clip)) = &self.play_clip { // Length of clip, to repeat or stop on end. let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); // Index of first sample to populate. let offset = self.clock().get_sample_offset(scope, &started); // Write MIDI events from clip at sample offsets corresponding to pulses. for (sample, pulse) in self.clock().get_pulses(scope, offset) { // If a next clip is enqueued, and we're past the end of the current one, // break the loop here (FIXME count pulse correctly) let past_end = if clip.is_some() { pulse >= length } else { true }; // Is it time for switchover? if self.next_clip().is_some() && past_end { return true } // If there's a currently playing clip, output notes from it to buffer: if let Some(clip) = clip { // Source clip from which the MIDI events will be taken. let clip = clip.read().unwrap(); // Clip with zero length is not processed if clip.length > 0 { // Current pulse index in source clip let pulse = pulse % clip.length; // Output each MIDI event from clip at appropriate frames of output buffer: for message in clip.notes[pulse].iter() { for port in self.midi_outs.iter_mut() { port.buffer_write(sample, LiveEvent::Midi { channel: 0.into(), /* TODO */ message: *message }); } } } } } false } else { true } } /// Handle switchover from current to next playing clip. fn process_switchover (&mut self, scope: &ProcessScope) { let midi_buf = self.midi_buf_mut(); let sample0 = scope.last_frame_time() as usize; //let samples = scope.n_frames() as usize; if let Some((start_at, clip)) = &self.next_clip() { let start = start_at.sample.get() as usize; let sample = self.clock().started.read().unwrap() .as_ref().unwrap().sample.get() as usize; // If it's time to switch to the next clip: if start <= sample0.saturating_sub(sample) { // Samples elapsed since clip was supposed to start let _skipped = sample0 - start; // Switch over to enqueued clip let started = Moment::from_sample(self.clock().timebase(), start as f64); // Launch enqueued clip *self.play_clip_mut() = Some((started, clip.clone())); // Unset enqueuement (TODO: where to implement looping?) *self.next_clip_mut() = None; // Fill in remaining ticks of chunk from next clip. self.process_playback(scope); } } } } impl HasMidiBuffers for Sequencer { fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } } impl> + Send + Sync> HasTracks for T {} impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } impl> HasTrack for T { fn track (&self) -> Option<&Track> { self.get() } fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } } has!(Clock: |self: Track|self.sequencer.clock); has!(Sequencer: |self: Track|self.sequencer); impl Track { /// Create a new track with only the default [Sequencer]. pub fn new ( name: &impl AsRef, color: Option, jack: &Jack<'static>, clock: Option<&Clock>, clip: Option<&Arc>>, midi_from: &[Connect], midi_to: &[Connect], ) -> Usually { Ok(Self { name: name.as_ref().into(), color: color.unwrap_or_default(), sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, ..Default::default() }) } pub fn audio_ins (&self) -> &[AudioInput] { self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() } pub fn audio_outs (&self) -> &[AudioOutput] { self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() } fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } } impl HasWidth for Track { const MIN_WIDTH: usize = 9; fn width_inc (&mut self) { self.width += 1; } fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } } #[cfg(feature = "sampler")] impl Track { /// Create a new track connecting the [Sequencer] to a [Sampler]. pub fn new_with_sampler ( name: &impl AsRef, color: Option, jack: &Jack<'static>, clock: Option<&Clock>, clip: Option<&Arc>>, midi_from: &[Connect], midi_to: &[Connect], audio_from: &[&[Connect];2], audio_to: &[&[Connect];2], ) -> Usually { let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; let client_name = jack.with_client(|c|c.name().to_string()); let port_name = track.sequencer.midi_outs[0].port_name(); let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; track.devices.push(Device::Sampler(Sampler::new( jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to )?)); Ok(track) } pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { for device in self.devices.iter() { match device { Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, _ => {} } } None } pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { for device in self.devices.iter_mut() { match device { Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, _ => {} } } None } } impl ClipsView for T {} impl Track { pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Content + 'a { Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ let width = (x2 - x1) as u16; map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( track.color.lightest.rgb, track.color.base.rgb, callback(index, track))))}) } }