mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 16:29:04 +01:00
might join app + device + engine again?
This commit is contained in:
parent
37068784cb
commit
604a42a4bc
27 changed files with 5963 additions and 5944 deletions
614
device/.scratch.rs
Normal file
614
device/.scratch.rs
Normal file
|
|
@ -0,0 +1,614 @@
|
||||||
|
//fn begin (browse: &mut Browse) => {
|
||||||
|
//unreachable!();
|
||||||
|
//}
|
||||||
|
//fn cancel (browse: &mut Browse) => {
|
||||||
|
//todo!()
|
||||||
|
////browse.mode = None;
|
||||||
|
////Ok(None)
|
||||||
|
//}
|
||||||
|
//fn confirm (browse: &mut Browse) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////if browse.is_file() {
|
||||||
|
////let path = browse.path();
|
||||||
|
////browse.mode = None;
|
||||||
|
////let _undo = PoolClipCommand::import(browse, index, path)?;
|
||||||
|
////None
|
||||||
|
////} else if browse.is_dir() {
|
||||||
|
////browse.mode = Some(PoolMode::Import(index, browse.chdir()?));
|
||||||
|
////None
|
||||||
|
////} else {
|
||||||
|
////None
|
||||||
|
////}
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////todo!()
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn select (browse: &mut Browse, index: usize) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////browse.index = index;
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////browse.index = index;
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn chdir (browse: &mut Browse, dir: PathBuf) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn filter (browse: &mut Browse, filter: Arc<str>) => {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//def_command!(ArrangementCommand: |arranger: Arrangement| {
|
||||||
|
|
||||||
|
//Home => { arranger.editor = None; Ok(None) },
|
||||||
|
|
||||||
|
//Edit => {
|
||||||
|
//let selection = arranger.selection().clone();
|
||||||
|
//arranger.editor = if arranger.editor.is_some() {
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//match selection {
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//let clip = &mut arranger.scenes_mut()[scene].clips[track];
|
||||||
|
//if clip.is_none() {
|
||||||
|
////app.clip_auto_create();
|
||||||
|
//*clip = Some(Arc::new(RwLock::new(MidiClip::new(
|
||||||
|
//&format!("t{track:02}s{scene:02}"),
|
||||||
|
//false, 384, None, Some(ItemTheme::random())
|
||||||
|
//))));
|
||||||
|
//}
|
||||||
|
//clip.as_ref().map(|c|c.into())
|
||||||
|
//}
|
||||||
|
//_ => {
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//};
|
||||||
|
//if let Some(editor) = arranger.editor.as_mut() {
|
||||||
|
//if let Some(clip) = editor.clip() {
|
||||||
|
//let length = clip.read().unwrap().length.max(1);
|
||||||
|
//let width = arranger.size_inner.w().saturating_sub(20).max(1);
|
||||||
|
//editor.set_time_zoom(length / width);
|
||||||
|
//editor.redraw();
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
////// Set the selection
|
||||||
|
//Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) },
|
||||||
|
|
||||||
|
////// Launch the selected clip or scene
|
||||||
|
//Launch => {
|
||||||
|
//match *arranger.selection() {
|
||||||
|
//Selection::Track(t) => {
|
||||||
|
//arranger.tracks[t].sequencer.enqueue_next(None)
|
||||||
|
//},
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref())
|
||||||
|
//},
|
||||||
|
//Selection::Scene(s) => {
|
||||||
|
//for t in 0..arranger.tracks.len() {
|
||||||
|
//arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => {}
|
||||||
|
//};
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
////// Set the color of the selected entity
|
||||||
|
//SetColor { palette: Option<ItemTheme> } => {
|
||||||
|
//let mut palette = palette.unwrap_or_else(||ItemTheme::random());
|
||||||
|
//let selection = *arranger.selection();
|
||||||
|
//Ok(Some(Self::SetColor { palette: Some(match selection {
|
||||||
|
//Selection::Mix => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.color);
|
||||||
|
//palette
|
||||||
|
//},
|
||||||
|
//Selection::Scene(s) => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.scenes[s].color);
|
||||||
|
//palette
|
||||||
|
//}
|
||||||
|
//Selection::Track(t) => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.tracks[t].color);
|
||||||
|
//palette
|
||||||
|
//}
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//if let Some(ref clip) = arranger.scenes[scene].clips[track] {
|
||||||
|
//let mut clip = clip.write().unwrap();
|
||||||
|
//std::mem::swap(&mut palette, &mut clip.color);
|
||||||
|
//palette
|
||||||
|
//} else {
|
||||||
|
//return Ok(None)
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => todo!()
|
||||||
|
//}) }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Track { track: TrackCommand } => { todo!("delegate") },
|
||||||
|
|
||||||
|
//TrackAdd => {
|
||||||
|
//let index = arranger.track_add(None, None, &[], &[])?.0;
|
||||||
|
//*arranger.selection_mut() = match arranger.selection() {
|
||||||
|
//Selection::Track(_) => Selection::Track(index),
|
||||||
|
//Selection::TrackClip { track: _, scene } => Selection::TrackClip {
|
||||||
|
//track: index, scene: *scene
|
||||||
|
//},
|
||||||
|
//_ => *arranger.selection()
|
||||||
|
//};
|
||||||
|
//Ok(Some(Self::TrackDelete { index }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//TrackSwap { index: usize, other: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let other = *other;
|
||||||
|
//Ok(Some(Self::TrackSwap { index, other }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//TrackDelete { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let exists = arranger.tracks().get(index).is_some();
|
||||||
|
//if exists {
|
||||||
|
//let track = arranger.tracks_mut().remove(index);
|
||||||
|
//let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
|
||||||
|
//for port in midi_ins.into_iter() {
|
||||||
|
//port.close()?;
|
||||||
|
//}
|
||||||
|
//for port in midi_outs.into_iter() {
|
||||||
|
//port.close()?;
|
||||||
|
//}
|
||||||
|
//for scene in arranger.scenes_mut().iter_mut() {
|
||||||
|
//scene.clips.remove(index);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) })
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiIn { input: MidiInputCommand } => {
|
||||||
|
//todo!("delegate"); Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiInAdd => {
|
||||||
|
//arranger.midi_in_add()?;
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiOut { output: MidiOutputCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiOutAdd => {
|
||||||
|
//arranger.midi_out_add()?;
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Device { command: DeviceCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//DeviceAdd { index: usize } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Scene { scene: SceneCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//OutputAdd => {
|
||||||
|
//arranger.midi_outs.push(MidiOutput::new(
|
||||||
|
//arranger.jack(),
|
||||||
|
//&format!("/M{}", arranger.midi_outs.len() + 1),
|
||||||
|
//&[]
|
||||||
|
//)?);
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//InputAdd => {
|
||||||
|
//arranger.midi_ins.push(MidiInput::new(
|
||||||
|
//arranger.jack(),
|
||||||
|
//&format!("M{}/", arranger.midi_ins.len() + 1),
|
||||||
|
//&[]
|
||||||
|
//)?);
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneAdd => {
|
||||||
|
//let index = arranger.scene_add(None, None)?.0;
|
||||||
|
//*arranger.selection_mut() = match arranger.selection() {
|
||||||
|
//Selection::Scene(_) => Selection::Scene(index),
|
||||||
|
//Selection::TrackClip { track, scene } => Selection::TrackClip {
|
||||||
|
//track: *track,
|
||||||
|
//scene: index
|
||||||
|
//},
|
||||||
|
//_ => *arranger.selection()
|
||||||
|
//};
|
||||||
|
//Ok(None) // TODO
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneSwap { index: usize, other: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let other = *other;
|
||||||
|
//Ok(Some(Self::SceneSwap { index, other }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneDelete { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let scenes = arranger.scenes_mut();
|
||||||
|
//Ok(if scenes.get(index).is_some() {
|
||||||
|
//let _scene = scenes.remove(index);
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//})
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneLaunch { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//for track in 0..arranger.tracks.len() {
|
||||||
|
//let clip = arranger.scenes[index].clips[track].as_ref();
|
||||||
|
//arranger.tracks[track].sequencer.enqueue_next(clip);
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Clip { scene: ClipCommand } => {
|
||||||
|
//todo!("delegate")
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipGet { a: usize, b: usize } => {
|
||||||
|
////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}"))
|
||||||
|
////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipPut { a: usize, b: usize } => {
|
||||||
|
////(Put [t: usize, s: usize, c: MaybeClip]
|
||||||
|
////Some(Self::Put(t, s, arranger.clip_put(t, s, c))))
|
||||||
|
////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipDel { a: usize, b: usize } => {
|
||||||
|
////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None))))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipEnqueue { a: usize, b: usize } => {
|
||||||
|
////(Enqueue [t: usize, s: usize]
|
||||||
|
////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())))
|
||||||
|
////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipSwap { a: usize, b: usize }=> {
|
||||||
|
////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}"))
|
||||||
|
////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//});
|
||||||
|
// Update sequencer playhead indicator
|
||||||
|
//self.now().set(0.);
|
||||||
|
//if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip {
|
||||||
|
//let clip = clip.read().unwrap();
|
||||||
|
//if *playing.read().unwrap() == *clip {
|
||||||
|
//let pulse = self.current().pulse.get();
|
||||||
|
//let start = started_at.pulse.get();
|
||||||
|
//let now = (pulse - start) % clip.length as f64;
|
||||||
|
//self.now().set(now);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
||||||
|
//let counts = plugin.port_counts();
|
||||||
|
//let mut jack = Jack::new(name)?;
|
||||||
|
//for i in 0..counts.atom_sequence_inputs {
|
||||||
|
//jack = jack.midi_in(&format!("midi-in-{i}"))
|
||||||
|
//}
|
||||||
|
//for i in 0..counts.atom_sequence_outputs {
|
||||||
|
//jack = jack.midi_out(&format!("midi-out-{i}"));
|
||||||
|
//}
|
||||||
|
//for i in 0..counts.audio_inputs {
|
||||||
|
//jack = jack.audio_in(&format!("audio-in-{i}"));
|
||||||
|
//}
|
||||||
|
//for i in 0..counts.audio_outputs {
|
||||||
|
//jack = jack.audio_out(&format!("audio-out-{i}"));
|
||||||
|
//}
|
||||||
|
//Ok(jack)
|
||||||
|
//}
|
||||||
|
//handle!(TuiIn: |self:Plugin, from|{
|
||||||
|
//match from.event() {
|
||||||
|
//kpat!(KeyCode::Up) => {
|
||||||
|
//self.selected = self.selected.saturating_sub(1);
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//kpat!(KeyCode::Down) => {
|
||||||
|
//self.selected = (self.selected + 1).min(match &self.plugin {
|
||||||
|
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||||
|
//_ => unimplemented!()
|
||||||
|
//});
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//kpat!(KeyCode::PageUp) => {
|
||||||
|
//self.selected = self.selected.saturating_sub(8);
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//kpat!(KeyCode::PageDown) => {
|
||||||
|
//self.selected = (self.selected + 10).min(match &self.plugin {
|
||||||
|
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||||
|
//_ => unimplemented!()
|
||||||
|
//});
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//kpat!(KeyCode::Char(',')) => {
|
||||||
|
//match self.plugin.as_mut() {
|
||||||
|
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||||
|
//let index = port_list[self.selected].index;
|
||||||
|
//if let Some(value) = instance.control_input(index) {
|
||||||
|
//instance.set_control_input(index, value - 0.01);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => {}
|
||||||
|
//}
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//kpat!(KeyCode::Char('.')) => {
|
||||||
|
//match self.plugin.as_mut() {
|
||||||
|
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||||
|
//let index = port_list[self.selected].index;
|
||||||
|
//if let Some(value) = instance.control_input(index) {
|
||||||
|
//instance.set_control_input(index, value + 0.01);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => {}
|
||||||
|
//}
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//kpat!(KeyCode::Char('g')) => {
|
||||||
|
//match self.plugin {
|
||||||
|
////Some(PluginKind::LV2(ref mut plugin)) => {
|
||||||
|
////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
|
||||||
|
////},
|
||||||
|
//Some(_) => unreachable!(),
|
||||||
|
//None => {}
|
||||||
|
//}
|
||||||
|
//Ok(Some(true))
|
||||||
|
//},
|
||||||
|
//_ => Ok(None)
|
||||||
|
//}
|
||||||
|
//});
|
||||||
|
|
||||||
|
//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
|
||||||
|
//let mut name = String::new();
|
||||||
|
//let mut path = String::new();
|
||||||
|
//atom!(atom in args {
|
||||||
|
//Atom::Map(map) => {
|
||||||
|
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
|
||||||
|
//name = String::from(*n);
|
||||||
|
//}
|
||||||
|
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
|
||||||
|
//path = String::from(*p);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => panic!("unexpected in lv2 '{name}'"),
|
||||||
|
//});
|
||||||
|
//Plugin::new_lv2(jack, &name, &path)
|
||||||
|
//});
|
||||||
|
|
||||||
|
//pub struct LV2PluginUI {
|
||||||
|
//write: (),
|
||||||
|
//controller: (),
|
||||||
|
//widget: (),
|
||||||
|
//features: (),
|
||||||
|
//transfer: (),
|
||||||
|
//}
|
||||||
|
//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref()
|
||||||
|
//.map(|p|Take::take(p, iter))
|
||||||
|
//.transpose()?
|
||||||
|
//.flatten()));
|
||||||
|
|
||||||
|
//fn file_browser_filter (&self) -> Arc<str> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//fn file_browser_path (&self) -> PathBuf {
|
||||||
|
//todo!();
|
||||||
|
//}
|
||||||
|
///// Immutable reference to sample at cursor.
|
||||||
|
//fn sample_selected (&self) -> Option<Arc<RwLock<Sample>>> {
|
||||||
|
//for (i, sample) in self.mapped.iter().enumerate() {
|
||||||
|
//if i == self.cursor().0 {
|
||||||
|
//return sample.as_ref()
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//for (i, sample) in self.unmapped.iter().enumerate() {
|
||||||
|
//if i + self.mapped.len() == self.cursor().0 {
|
||||||
|
//return Some(sample)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//fn sample_gain (&self) -> f32 {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//fn sample_above () -> usize {
|
||||||
|
//self.note_pos().min(119) + 8
|
||||||
|
//}
|
||||||
|
//fn sample_below () -> usize {
|
||||||
|
//self.note_pos().max(8) - 8
|
||||||
|
//}
|
||||||
|
//fn sample_to_left () -> usize {
|
||||||
|
//self.note_pos().min(126) + 1
|
||||||
|
//}
|
||||||
|
//fn sample_to_right () -> usize {
|
||||||
|
//self.note_pos().max(1) - 1
|
||||||
|
//}
|
||||||
|
//fn selected_pitch () -> u7 {
|
||||||
|
//(self.note_pos() as u8).into() // TODO
|
||||||
|
//}
|
||||||
|
|
||||||
|
//select (&self, state: &mut Sampler, i: usize) -> Option<Self> {
|
||||||
|
//Self::Select(state.set_note_pos(i))
|
||||||
|
//}
|
||||||
|
///// Assign sample to slot
|
||||||
|
//set (&self, slot: u7, sample: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
||||||
|
//let i = slot.as_int() as usize;
|
||||||
|
//let old = self.mapped[i].clone();
|
||||||
|
//self.mapped[i] = sample;
|
||||||
|
//Some(Self::Set(old))
|
||||||
|
//}
|
||||||
|
//set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//note_off (&self, state: &mut Sampler, slot: u7) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//set_sample (&self, state: &mut Sampler, slot: u7, s: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
||||||
|
//Some(Self::SetSample(p, state.set_sample(p, s)))
|
||||||
|
//}
|
||||||
|
//import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> {
|
||||||
|
//match c {
|
||||||
|
//FileBrowserCommand::Begin => {
|
||||||
|
////let voices = &state.state.voices;
|
||||||
|
////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||||
|
//state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
|
||||||
|
//None
|
||||||
|
//},
|
||||||
|
//_ => {
|
||||||
|
//println!("\n\rtodo: import: filebrowser: {c:?}");
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
////(Select [i: usize] Some(Self::Select(state.set_note_pos(i))))
|
||||||
|
////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize)))
|
||||||
|
////(RecordCancel [] cmd!(state.cancel_recording()))
|
||||||
|
////(RecordFinish [] cmd!(state.finish_recording()))
|
||||||
|
////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(SetSample [p: u7, s: Option<Arc<RwLock<Sample>>>] Some(Self::SetSample(p, state.set_sample(p, s))))
|
||||||
|
////(Import [c: FileBrowserCommand] match c {
|
||||||
|
////FileBrowserCommand::Begin => {
|
||||||
|
//////let voices = &state.state.voices;
|
||||||
|
//////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||||
|
////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => {
|
||||||
|
////println!("\n\rtodo: import: filebrowser: {c:?}");
|
||||||
|
////None
|
||||||
|
////}
|
||||||
|
////})));
|
||||||
|
////("import" [,..a]
|
||||||
|
////FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
||||||
|
////("select" [i: usize]
|
||||||
|
////Some(Self::Select(i.expect("no index"))))
|
||||||
|
////("record/begin" [i: u7]
|
||||||
|
////Some(Self::RecordBegin(i.expect("no index"))))
|
||||||
|
////("record/cancel" []
|
||||||
|
////Some(Self::RecordCancel))
|
||||||
|
////("record/finish" []
|
||||||
|
////Some(Self::RecordFinish))
|
||||||
|
////("set/sample" [i: u7, s: Option<Arc<RwLock<Sample>>>]
|
||||||
|
////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler"))))
|
||||||
|
////("set/start" [i: u7, s: usize]
|
||||||
|
////Some(Self::SetStart(i.expect("no index"), s.expect("no start"))))
|
||||||
|
////("set/gain" [i: u7, g: f32]
|
||||||
|
////Some(Self::SetGain(i.expect("no index"), g.expect("no gain"))))
|
||||||
|
////("note/on" [p: u7, v: u7]
|
||||||
|
////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity"))))
|
||||||
|
////("note/off" [p: u7]
|
||||||
|
////Some(Self::NoteOff(p.expect("no slot"))))));
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
//for port in midi_in.iter() {
|
||||||
|
//for event in port.iter() {
|
||||||
|
//match event {
|
||||||
|
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
|
||||||
|
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
|
||||||
|
//editor.set_note_pos(key.as_int() as usize);
|
||||||
|
//},
|
||||||
|
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
|
||||||
|
//self.editor.as_ref(),
|
||||||
|
//self.sampler.as_ref(),
|
||||||
|
//) => {
|
||||||
|
//// TODO: give sampler its own cursor
|
||||||
|
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
|
||||||
|
//sample.write().unwrap().handle_cc(*controller, *value)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//_ =>{}
|
||||||
|
//},
|
||||||
|
//_ =>{}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//scene_scroll: Fill::Y(Fixed::X(1, ScrollbarV {
|
||||||
|
//offset: arrangement.scene_scroll,
|
||||||
|
//length: h_scenes_area as usize,
|
||||||
|
//total: h_scenes as usize,
|
||||||
|
//})),
|
||||||
|
//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref()
|
||||||
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
|
|
||||||
|
//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
||||||
|
//fg: Color, bg: Color, iter: &mut impl Iterator<Item = (usize, &'a Arc<str>, &'a [Connect], usize, usize)>
|
||||||
|
//) -> impl Content<TuiOut> + 'a {
|
||||||
|
//Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter {
|
||||||
|
//to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||||
|
//Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"▞▞▞▞ ▞▞▞▞"))))),
|
||||||
|
//Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() {
|
||||||
|
//to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false,
|
||||||
|
//wrap(bg, fg, Fill::Y(&"")))))))
|
||||||
|
//})
|
||||||
|
//)))
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH {
|
||||||
|
//offset: arrangement.track_scroll,
|
||||||
|
//length: h_tracks_area as usize,
|
||||||
|
//total: h_scenes as usize,
|
||||||
|
//})),
|
||||||
|
//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref()
|
||||||
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
|
|
@ -1,650 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(Default, Debug)] pub struct Arrangement {
|
|
||||||
/// Project name.
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Base color.
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// JACK client handle.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// FIXME a render of the project arrangement, redrawn on update.
|
|
||||||
/// TODO rename to "render_cache" or smth
|
|
||||||
pub arranger: Arc<RwLock<Buffer>>,
|
|
||||||
/// Display size
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// Display size of clips area
|
|
||||||
pub size_inner: Measure<TuiOut>,
|
|
||||||
/// Source of time
|
|
||||||
#[cfg(feature = "clock")] pub clock: Clock,
|
|
||||||
/// Allows one MIDI clip to be edited
|
|
||||||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
|
||||||
/// List of global midi inputs
|
|
||||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
|
||||||
/// List of global midi outputs
|
|
||||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
|
||||||
/// List of global audio inputs
|
|
||||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
|
||||||
/// List of global audio outputs
|
|
||||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
|
||||||
/// Selected UI element
|
|
||||||
#[cfg(feature = "select")] pub selection: Selection,
|
|
||||||
/// Last track number (to avoid duplicate port names)
|
|
||||||
#[cfg(feature = "track")] pub track_last: usize,
|
|
||||||
/// List of tracks
|
|
||||||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
|
||||||
/// Scroll offset of tracks
|
|
||||||
#[cfg(feature = "track")] pub track_scroll: usize,
|
|
||||||
/// List of scenes
|
|
||||||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
|
||||||
/// Scroll offset of scenes
|
|
||||||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
|
||||||
}
|
|
||||||
impl HasJack<'static> for Arrangement {
|
|
||||||
fn jack (&self) -> &Jack<'static> {
|
|
||||||
&self.jack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has!(Jack<'static>: |self: Arrangement|self.jack);
|
|
||||||
has!(Measure<TuiOut>: |self: Arrangement|self.size);
|
|
||||||
|
|
||||||
#[cfg(feature = "editor")] has!(Option<MidiEditor>: |self: Arrangement|self.editor);
|
|
||||||
#[cfg(feature = "port")] has!(Vec<MidiInput>: |self: Arrangement|self.midi_ins);
|
|
||||||
#[cfg(feature = "port")] has!(Vec<MidiOutput>: |self: Arrangement|self.midi_outs);
|
|
||||||
#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock);
|
|
||||||
#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection);
|
|
||||||
|
|
||||||
#[cfg(all(feature = "select", feature = "track"))] has!(Vec<Track>: |self: Arrangement|self.tracks);
|
|
||||||
#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement|
|
|
||||||
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Track>>::get(self).get(index)).flatten() };
|
|
||||||
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Track>>::get_mut(self).get_mut(index)).flatten() });
|
|
||||||
|
|
||||||
#[cfg(all(feature = "select", feature = "scene"))] has!(Vec<Scene>: |self: Arrangement|self.scenes);
|
|
||||||
#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement|
|
|
||||||
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Scene>>::get(self).get(index)).flatten() };
|
|
||||||
{ Has::<Selection>::get(self).track().map(|index|Has::<Vec<Scene>>::get_mut(self).get_mut(index)).flatten() });
|
|
||||||
|
|
||||||
#[cfg(feature = "select")] impl Arrangement {
|
|
||||||
#[cfg(feature = "clip")] fn selected_clip (&self) -> Option<MidiClip> { todo!() }
|
|
||||||
#[cfg(feature = "scene")] fn selected_scene (&self) -> Option<Scene> { todo!() }
|
|
||||||
#[cfg(feature = "track")] fn selected_track (&self) -> Option<Track> { todo!() }
|
|
||||||
#[cfg(feature = "port")] fn selected_midi_in (&self) -> Option<MidiInput> { todo!() }
|
|
||||||
#[cfg(feature = "port")] fn selected_midi_out (&self) -> Option<MidiOutput> { todo!() }
|
|
||||||
fn selected_device (&self) -> Option<Device> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn unselect (&self) -> Selection {
|
|
||||||
Selection::Nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Arrangement {
|
|
||||||
/// Create a new arrangement.
|
|
||||||
pub fn new (
|
|
||||||
jack: &Jack<'static>,
|
|
||||||
name: Option<Arc<str>>,
|
|
||||||
clock: Clock,
|
|
||||||
tracks: Vec<Track>,
|
|
||||||
scenes: Vec<Scene>,
|
|
||||||
midi_ins: Vec<MidiInput>,
|
|
||||||
midi_outs: Vec<MidiOutput>,
|
|
||||||
) -> 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 TracksView for Arrangement {}
|
|
||||||
|
|
||||||
#[cfg(feature = "track")]
|
|
||||||
impl Arrangement {
|
|
||||||
/// Get the active track
|
|
||||||
pub fn get_track (&self) -> Option<&Track> {
|
|
||||||
let index = self.selection().track()?;
|
|
||||||
Has::<Vec<Track>>::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::<Vec<Track>>::get_mut(self).get_mut(index)
|
|
||||||
}
|
|
||||||
/// Add multiple tracks
|
|
||||||
pub fn tracks_add (
|
|
||||||
&mut self,
|
|
||||||
count: usize, width: Option<usize>,
|
|
||||||
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<ItemTheme>,
|
|
||||||
mins: &[Connect], mouts: &[Connect],
|
|
||||||
) -> Usually<(usize, &mut Track)> {
|
|
||||||
let name: Arc<str> = 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<TuiOut> + '_ {
|
|
||||||
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<TuiOut> + '_ {
|
|
||||||
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<TuiOut> {
|
|
||||||
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<TuiOut> {
|
|
||||||
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<TuiOut> {
|
|
||||||
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 = "track")]
|
|
||||||
pub fn view_track_row_section (
|
|
||||||
_theme: ItemTheme,
|
|
||||||
button: impl Content<TuiOut>,
|
|
||||||
button_add: impl Content<TuiOut>,
|
|
||||||
content: impl Content<TuiOut>,
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))),
|
|
||||||
Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "scene")]
|
|
||||||
impl Arrangement {
|
|
||||||
/// Get the active scene
|
|
||||||
pub fn get_scene (&self) -> Option<&Scene> {
|
|
||||||
let index = self.selection().scene()?;
|
|
||||||
Has::<Vec<Scene>>::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::<Vec<Scene>>::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<Arc<RwLock<MidiClip>>> {
|
|
||||||
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<Arc<RwLock<MidiClip>>>
|
|
||||||
) -> Option<Arc<RwLock<MidiClip>>> {
|
|
||||||
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<ItemTheme>
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
|
||||||
let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▐")));
|
|
||||||
let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▌")));
|
|
||||||
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasClipsSize {
|
|
||||||
fn clips_size (&self) -> &Measure<TuiOut>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasClipsSize for Arrangement {
|
|
||||||
fn clips_size (&self) -> &Measure<TuiOut> { &self.size_inner }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasWidth {
|
|
||||||
const MIN_WIDTH: usize;
|
|
||||||
/// Increment track width.
|
|
||||||
fn width_inc (&mut self);
|
|
||||||
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
|
||||||
fn width_dec (&mut self);
|
|
||||||
}
|
|
||||||
|
|
||||||
//def_command!(ArrangementCommand: |arranger: Arrangement| {
|
|
||||||
|
|
||||||
//Home => { arranger.editor = None; Ok(None) },
|
|
||||||
|
|
||||||
//Edit => {
|
|
||||||
//let selection = arranger.selection().clone();
|
|
||||||
//arranger.editor = if arranger.editor.is_some() {
|
|
||||||
//None
|
|
||||||
//} else {
|
|
||||||
//match selection {
|
|
||||||
//Selection::TrackClip { track, scene } => {
|
|
||||||
//let clip = &mut arranger.scenes_mut()[scene].clips[track];
|
|
||||||
//if clip.is_none() {
|
|
||||||
////app.clip_auto_create();
|
|
||||||
//*clip = Some(Arc::new(RwLock::new(MidiClip::new(
|
|
||||||
//&format!("t{track:02}s{scene:02}"),
|
|
||||||
//false, 384, None, Some(ItemTheme::random())
|
|
||||||
//))));
|
|
||||||
//}
|
|
||||||
//clip.as_ref().map(|c|c.into())
|
|
||||||
//}
|
|
||||||
//_ => {
|
|
||||||
//None
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//};
|
|
||||||
//if let Some(editor) = arranger.editor.as_mut() {
|
|
||||||
//if let Some(clip) = editor.clip() {
|
|
||||||
//let length = clip.read().unwrap().length.max(1);
|
|
||||||
//let width = arranger.size_inner.w().saturating_sub(20).max(1);
|
|
||||||
//editor.set_time_zoom(length / width);
|
|
||||||
//editor.redraw();
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
////// Set the selection
|
|
||||||
//Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) },
|
|
||||||
|
|
||||||
////// Launch the selected clip or scene
|
|
||||||
//Launch => {
|
|
||||||
//match *arranger.selection() {
|
|
||||||
//Selection::Track(t) => {
|
|
||||||
//arranger.tracks[t].sequencer.enqueue_next(None)
|
|
||||||
//},
|
|
||||||
//Selection::TrackClip { track, scene } => {
|
|
||||||
//arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref())
|
|
||||||
//},
|
|
||||||
//Selection::Scene(s) => {
|
|
||||||
//for t in 0..arranger.tracks.len() {
|
|
||||||
//arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => {}
|
|
||||||
//};
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
////// Set the color of the selected entity
|
|
||||||
//SetColor { palette: Option<ItemTheme> } => {
|
|
||||||
//let mut palette = palette.unwrap_or_else(||ItemTheme::random());
|
|
||||||
//let selection = *arranger.selection();
|
|
||||||
//Ok(Some(Self::SetColor { palette: Some(match selection {
|
|
||||||
//Selection::Mix => {
|
|
||||||
//std::mem::swap(&mut palette, &mut arranger.color);
|
|
||||||
//palette
|
|
||||||
//},
|
|
||||||
//Selection::Scene(s) => {
|
|
||||||
//std::mem::swap(&mut palette, &mut arranger.scenes[s].color);
|
|
||||||
//palette
|
|
||||||
//}
|
|
||||||
//Selection::Track(t) => {
|
|
||||||
//std::mem::swap(&mut palette, &mut arranger.tracks[t].color);
|
|
||||||
//palette
|
|
||||||
//}
|
|
||||||
//Selection::TrackClip { track, scene } => {
|
|
||||||
//if let Some(ref clip) = arranger.scenes[scene].clips[track] {
|
|
||||||
//let mut clip = clip.write().unwrap();
|
|
||||||
//std::mem::swap(&mut palette, &mut clip.color);
|
|
||||||
//palette
|
|
||||||
//} else {
|
|
||||||
//return Ok(None)
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => todo!()
|
|
||||||
//}) }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Track { track: TrackCommand } => { todo!("delegate") },
|
|
||||||
|
|
||||||
//TrackAdd => {
|
|
||||||
//let index = arranger.track_add(None, None, &[], &[])?.0;
|
|
||||||
//*arranger.selection_mut() = match arranger.selection() {
|
|
||||||
//Selection::Track(_) => Selection::Track(index),
|
|
||||||
//Selection::TrackClip { track: _, scene } => Selection::TrackClip {
|
|
||||||
//track: index, scene: *scene
|
|
||||||
//},
|
|
||||||
//_ => *arranger.selection()
|
|
||||||
//};
|
|
||||||
//Ok(Some(Self::TrackDelete { index }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//TrackSwap { index: usize, other: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let other = *other;
|
|
||||||
//Ok(Some(Self::TrackSwap { index, other }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//TrackDelete { index: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let exists = arranger.tracks().get(index).is_some();
|
|
||||||
//if exists {
|
|
||||||
//let track = arranger.tracks_mut().remove(index);
|
|
||||||
//let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
|
|
||||||
//for port in midi_ins.into_iter() {
|
|
||||||
//port.close()?;
|
|
||||||
//}
|
|
||||||
//for port in midi_outs.into_iter() {
|
|
||||||
//port.close()?;
|
|
||||||
//}
|
|
||||||
//for scene in arranger.scenes_mut().iter_mut() {
|
|
||||||
//scene.clips.remove(index);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) })
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiIn { input: MidiInputCommand } => {
|
|
||||||
//todo!("delegate"); Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiInAdd => {
|
|
||||||
//arranger.midi_in_add()?;
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiOut { output: MidiOutputCommand } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiOutAdd => {
|
|
||||||
//arranger.midi_out_add()?;
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Device { command: DeviceCommand } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//DeviceAdd { index: usize } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Scene { scene: SceneCommand } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//OutputAdd => {
|
|
||||||
//arranger.midi_outs.push(MidiOutput::new(
|
|
||||||
//arranger.jack(),
|
|
||||||
//&format!("/M{}", arranger.midi_outs.len() + 1),
|
|
||||||
//&[]
|
|
||||||
//)?);
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//InputAdd => {
|
|
||||||
//arranger.midi_ins.push(MidiInput::new(
|
|
||||||
//arranger.jack(),
|
|
||||||
//&format!("M{}/", arranger.midi_ins.len() + 1),
|
|
||||||
//&[]
|
|
||||||
//)?);
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneAdd => {
|
|
||||||
//let index = arranger.scene_add(None, None)?.0;
|
|
||||||
//*arranger.selection_mut() = match arranger.selection() {
|
|
||||||
//Selection::Scene(_) => Selection::Scene(index),
|
|
||||||
//Selection::TrackClip { track, scene } => Selection::TrackClip {
|
|
||||||
//track: *track,
|
|
||||||
//scene: index
|
|
||||||
//},
|
|
||||||
//_ => *arranger.selection()
|
|
||||||
//};
|
|
||||||
//Ok(None) // TODO
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneSwap { index: usize, other: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let other = *other;
|
|
||||||
//Ok(Some(Self::SceneSwap { index, other }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneDelete { index: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let scenes = arranger.scenes_mut();
|
|
||||||
//Ok(if scenes.get(index).is_some() {
|
|
||||||
//let _scene = scenes.remove(index);
|
|
||||||
//None
|
|
||||||
//} else {
|
|
||||||
//None
|
|
||||||
//})
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneLaunch { index: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//for track in 0..arranger.tracks.len() {
|
|
||||||
//let clip = arranger.scenes[index].clips[track].as_ref();
|
|
||||||
//arranger.tracks[track].sequencer.enqueue_next(clip);
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Clip { scene: ClipCommand } => {
|
|
||||||
//todo!("delegate")
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipGet { a: usize, b: usize } => {
|
|
||||||
////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}"))
|
|
||||||
////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipPut { a: usize, b: usize } => {
|
|
||||||
////(Put [t: usize, s: usize, c: MaybeClip]
|
|
||||||
////Some(Self::Put(t, s, arranger.clip_put(t, s, c))))
|
|
||||||
////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipDel { a: usize, b: usize } => {
|
|
||||||
////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None))))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipEnqueue { a: usize, b: usize } => {
|
|
||||||
////(Enqueue [t: usize, s: usize]
|
|
||||||
////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())))
|
|
||||||
////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipSwap { a: usize, b: usize }=> {
|
|
||||||
////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}"))
|
|
||||||
////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//});
|
|
||||||
212
device/browse.rs
212
device/browse.rs
|
|
@ -1,212 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::ffi::OsString;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum BrowseTarget {
|
|
||||||
SaveProject,
|
|
||||||
LoadProject,
|
|
||||||
ImportSample(Arc<RwLock<Option<Sample>>>),
|
|
||||||
ExportSample(Arc<RwLock<Option<Sample>>>),
|
|
||||||
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
|
||||||
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Browses for phrase to import/export
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
|
||||||
pub struct Browse {
|
|
||||||
pub cwd: PathBuf,
|
|
||||||
pub dirs: Vec<(OsString, String)>,
|
|
||||||
pub files: Vec<(OsString, String)>,
|
|
||||||
pub filter: String,
|
|
||||||
pub index: usize,
|
|
||||||
pub scroll: usize,
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Browse {
|
|
||||||
|
|
||||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
|
||||||
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(|_|"<unreadable>".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> {
|
|
||||||
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<str> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def_command!(BrowseCommand: |browse: Browse| {
|
|
||||||
SetVisible => Ok(None),
|
|
||||||
SetPath { address: PathBuf } => Ok(None),
|
|
||||||
SetSearch { filter: Arc<str> } => Ok(None),
|
|
||||||
SetCursor { cursor: usize } => Ok(None),
|
|
||||||
});
|
|
||||||
|
|
||||||
impl HasContent<TuiOut> for Browse {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
Map::south(1, ||EntriesIterator {
|
|
||||||
offset: 0,
|
|
||||||
index: 0,
|
|
||||||
length: self.dirs.len() + self.files.len(),
|
|
||||||
browser: self,
|
|
||||||
}, |entry, _index|Fill::X(Align::w(entry)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EntriesIterator<'a> {
|
|
||||||
browser: &'a Browse,
|
|
||||||
offset: usize,
|
|
||||||
length: usize,
|
|
||||||
index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for EntriesIterator<'a> {
|
|
||||||
type Item = Modify<&'a str>;
|
|
||||||
fn next (&mut self) -> Option<Self::Item> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commands supported by [Browse]
|
|
||||||
//#[derive(Debug, Clone, PartialEq)]
|
|
||||||
//pub enum BrowseCommand {
|
|
||||||
//Begin,
|
|
||||||
//Cancel,
|
|
||||||
//Confirm,
|
|
||||||
//Select(usize),
|
|
||||||
//Chdir(PathBuf),
|
|
||||||
//Filter(Arc<str>),
|
|
||||||
//}
|
|
||||||
//fn begin (browse: &mut Browse) => {
|
|
||||||
//unreachable!();
|
|
||||||
//}
|
|
||||||
//fn cancel (browse: &mut Browse) => {
|
|
||||||
//todo!()
|
|
||||||
////browse.mode = None;
|
|
||||||
////Ok(None)
|
|
||||||
//}
|
|
||||||
//fn confirm (browse: &mut Browse) => {
|
|
||||||
//todo!()
|
|
||||||
////Ok(match browse.mode {
|
|
||||||
////Some(PoolMode::Import(index, ref mut browse)) => {
|
|
||||||
////if browse.is_file() {
|
|
||||||
////let path = browse.path();
|
|
||||||
////browse.mode = None;
|
|
||||||
////let _undo = PoolClipCommand::import(browse, index, path)?;
|
|
||||||
////None
|
|
||||||
////} else if browse.is_dir() {
|
|
||||||
////browse.mode = Some(PoolMode::Import(index, browse.chdir()?));
|
|
||||||
////None
|
|
||||||
////} else {
|
|
||||||
////None
|
|
||||||
////}
|
|
||||||
////},
|
|
||||||
////Some(PoolMode::Export(index, ref mut browse)) => {
|
|
||||||
////todo!()
|
|
||||||
////},
|
|
||||||
////_ => unreachable!(),
|
|
||||||
////})
|
|
||||||
//}
|
|
||||||
//fn select (browse: &mut Browse, index: usize) => {
|
|
||||||
//todo!()
|
|
||||||
////Ok(match browse.mode {
|
|
||||||
////Some(PoolMode::Import(index, ref mut browse)) => {
|
|
||||||
////browse.index = index;
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////Some(PoolMode::Export(index, ref mut browse)) => {
|
|
||||||
////browse.index = index;
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////_ => unreachable!(),
|
|
||||||
////})
|
|
||||||
//}
|
|
||||||
//fn chdir (browse: &mut Browse, dir: PathBuf) => {
|
|
||||||
//todo!()
|
|
||||||
////Ok(match browse.mode {
|
|
||||||
////Some(PoolMode::Import(index, ref mut browse)) => {
|
|
||||||
////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?));
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////Some(PoolMode::Export(index, ref mut browse)) => {
|
|
||||||
////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?));
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////_ => unreachable!(),
|
|
||||||
////})
|
|
||||||
//}
|
|
||||||
//fn filter (browse: &mut Browse, filter: Arc<str>) => {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
215
device/clip.rs
215
device/clip.rs
|
|
@ -1,215 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub trait HasMidiClip {
|
|
||||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_clip {
|
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A MIDI sequence.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct MidiClip {
|
|
||||||
pub uuid: uuid::Uuid,
|
|
||||||
/// Name of clip
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Temporal resolution in pulses per quarter note
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Length of clip in pulses
|
|
||||||
pub length: usize,
|
|
||||||
/// Notes in clip
|
|
||||||
pub notes: MidiData,
|
|
||||||
/// Whether to loop the clip or play it once
|
|
||||||
pub looped: bool,
|
|
||||||
/// Start of loop
|
|
||||||
pub loop_start: usize,
|
|
||||||
/// Length of loop
|
|
||||||
pub loop_length: usize,
|
|
||||||
/// All notes are displayed with minimum length
|
|
||||||
pub percussive: bool,
|
|
||||||
/// Identifying color of clip
|
|
||||||
pub color: ItemTheme,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI message structural
|
|
||||||
pub type MidiData = Vec<Vec<MidiMessage>>;
|
|
||||||
|
|
||||||
impl MidiClip {
|
|
||||||
pub fn new (
|
|
||||||
name: impl AsRef<str>,
|
|
||||||
looped: bool,
|
|
||||||
length: usize,
|
|
||||||
notes: Option<MidiData>,
|
|
||||||
color: Option<ItemTheme>,
|
|
||||||
) -> 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<bool> { todo!() }
|
|
||||||
fn _todo_bool_stub_ (&self) -> bool { todo!() }
|
|
||||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
|
||||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
|
||||||
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
|
||||||
fn _todo_opt_item_theme_stub (&self) -> Option<ItemTheme> { todo!() }
|
|
||||||
}
|
|
||||||
|
|
||||||
def_command!(ClipCommand: |clip: MidiClip| {
|
|
||||||
|
|
||||||
SetColor { color: Option<ItemTheme> } => {
|
|
||||||
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
|
||||||
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
|
||||||
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
|
||||||
todo!()
|
|
||||||
},
|
|
||||||
|
|
||||||
SetLoop { looping: Option<bool> } => {
|
|
||||||
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
|
||||||
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
pub trait ClipsView:
|
|
||||||
TracksView +
|
|
||||||
ScenesView +
|
|
||||||
HasClipsSize +
|
|
||||||
Send +
|
|
||||||
Sync
|
|
||||||
{
|
|
||||||
fn view_scenes_clips <'a> (&'a self)
|
|
||||||
-> impl Content<TuiOut> + 'a
|
|
||||||
{
|
|
||||||
self.clips_size().of(Fill::XY(Bsp::a(
|
|
||||||
Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
|
||||||
Thunk::new(|to: &mut TuiOut|for (
|
|
||||||
track_index, track, _, _
|
|
||||||
) in self.tracks_with_sizes() {
|
|
||||||
to.place(&Fixed::X(track.width as u16,
|
|
||||||
Fill::Y(self.view_track_clips(track_index, track))))
|
|
||||||
}))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
|
|
||||||
Thunk::new(move|to: &mut TuiOut|for (
|
|
||||||
scene_index, scene, ..
|
|
||||||
) in self.scenes_with_sizes() {
|
|
||||||
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
|
||||||
} else {
|
|
||||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
|
||||||
};
|
|
||||||
let fg = theme.lightest.rgb;
|
|
||||||
let mut outline = theme.base.rgb;
|
|
||||||
let bg = if self.selection().track() == Some(track_index)
|
|
||||||
&& self.selection().scene() == Some(scene_index)
|
|
||||||
{
|
|
||||||
outline = theme.lighter.rgb;
|
|
||||||
theme.light.rgb
|
|
||||||
} else if self.selection().track() == Some(track_index)
|
|
||||||
|| self.selection().scene() == Some(scene_index)
|
|
||||||
{
|
|
||||||
outline = theme.darkest.rgb;
|
|
||||||
theme.base.rgb
|
|
||||||
} else {
|
|
||||||
theme.dark.rgb
|
|
||||||
};
|
|
||||||
let w = if self.selection().track() == Some(track_index)
|
|
||||||
&& let Some(editor) = self.editor ()
|
|
||||||
{
|
|
||||||
(editor.measure_width() as usize).max(24).max(track.width)
|
|
||||||
} else {
|
|
||||||
track.width
|
|
||||||
} as u16;
|
|
||||||
let y = if self.selection().scene() == Some(scene_index)
|
|
||||||
&& let Some(editor) = self.editor ()
|
|
||||||
{
|
|
||||||
(editor.measure_height() as usize).max(12)
|
|
||||||
} else {
|
|
||||||
Self::H_SCENE as usize
|
|
||||||
} as u16;
|
|
||||||
|
|
||||||
to.place(&Fixed::XY(w, y, Bsp::b(
|
|
||||||
Fill::XY(Outer(true, Style::default().fg(outline))),
|
|
||||||
Fill::XY(Bsp::b(
|
|
||||||
Bsp::b(
|
|
||||||
Tui::fg_bg(outline, bg, Fill::XY("")),
|
|
||||||
Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
|
||||||
),
|
|
||||||
Fill::XY(When::new(self.selection().track() == Some(track_index)
|
|
||||||
&& self.selection().scene() == Some(scene_index)
|
|
||||||
&& self.is_editing(), self.editor())))))));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref()
|
|
||||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
|
||||||
421
device/clock.rs
421
device/clock.rs
|
|
@ -1,421 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
pub trait HasClock: Send + Sync {
|
|
||||||
fn clock (&self) -> &Clock;
|
|
||||||
fn clock_mut (&mut self) -> &mut Clock;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Has<Clock>> 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<Option<Transport>>,
|
|
||||||
/// Global temporal resolution (shared by [Moment] fields)
|
|
||||||
pub timebase: Arc<Timebase>,
|
|
||||||
/// Current global sample and usec (monotonic from JACK clock)
|
|
||||||
pub global: Arc<Moment>,
|
|
||||||
/// Global sample and usec at which playback started
|
|
||||||
pub started: Arc<RwLock<Option<Moment>>>,
|
|
||||||
/// Playback offset (when playing not from start)
|
|
||||||
pub offset: Arc<Moment>,
|
|
||||||
/// Current playhead position
|
|
||||||
pub playhead: Arc<Moment>,
|
|
||||||
/// Note quantization factor
|
|
||||||
pub quant: Arc<Quantize>,
|
|
||||||
/// Launch quantization factor
|
|
||||||
pub sync: Arc<LaunchSync>,
|
|
||||||
/// Size of buffer in samples
|
|
||||||
pub chunk: Arc<AtomicUsize>,
|
|
||||||
// Cache of formatted strings
|
|
||||||
pub view_cache: Arc<RwLock<ViewCache>>,
|
|
||||||
/// For syncing the clock to an external source
|
|
||||||
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
|
||||||
/// For syncing other devices to this clock
|
|
||||||
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
|
||||||
/// For emitting a metronome
|
|
||||||
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<f64>) -> Usually<Self> {
|
|
||||||
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<Timebase> {
|
|
||||||
&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<u32>) -> 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<u32>) -> 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<u32> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn _todo_provide_f64 (&self) -> f64 {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: HasClock> Command<T> for ClockCommand {
|
|
||||||
fn execute (&self, state: &mut T) -> Perhaps<Self> {
|
|
||||||
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<u32> } => {
|
|
||||||
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
|
||||||
Pause { position: Option<u32> } => {
|
|
||||||
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<RwLock<String>>,
|
|
||||||
beat: Arc<RwLock<String>>,
|
|
||||||
time: Arc<RwLock<String>>,
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
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<Arc<str>>,
|
|
||||||
sr: Arc<RwLock<String>>,
|
|
||||||
buf: Arc<RwLock<String>>,
|
|
||||||
lat: Arc<RwLock<String>>,
|
|
||||||
) -> impl Content<TuiOut> {
|
|
||||||
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 fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
|
|
||||||
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<Option<(bool, f64)>, String>,
|
|
||||||
pub buf: Memo<Option<f64>, String>,
|
|
||||||
pub lat: Memo<Option<f64>, String>,
|
|
||||||
pub bpm: Memo<Option<f64>, String>,
|
|
||||||
pub beat: Memo<Option<f64>, String>,
|
|
||||||
pub time: Memo<Option<f64>, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<RwLock<Self>>, track: usize, tracks: usize)
|
|
||||||
//-> Arc<RwLock<String>>
|
|
||||||
//{
|
|
||||||
//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<RwLock<Self>>, scene: usize, scenes: usize, is_editing: bool)
|
|
||||||
//-> impl Content<TuiOut>
|
|
||||||
//{
|
|
||||||
//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<RwLock<Self>>, clock: &Clock, compact: bool) {
|
|
||||||
let rate = clock.timebase.sr.get();
|
|
||||||
let chunk = clock.chunk.load(Relaxed) as f64;
|
|
||||||
let lat = chunk / rate * 1000.;
|
|
||||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
|
||||||
let mut cache = cache.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<TuiOut> {
|
|
||||||
//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)))));
|
|
||||||
////}
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
718
device/device.rs
718
device/device.rs
|
|
@ -1,6 +1,14 @@
|
||||||
#![feature(trait_alias)]
|
#![feature(trait_alias)]
|
||||||
|
|
||||||
pub use tek_engine;
|
pub use tek_engine;
|
||||||
|
pub(crate) use ConnectName::*;
|
||||||
|
pub(crate) use ConnectScope::*;
|
||||||
|
pub(crate) use ConnectStatus::*;
|
||||||
|
|
||||||
|
mod device_struct; pub use self::device_struct::*;
|
||||||
|
mod device_trait; pub use self::device_trait::*;
|
||||||
|
mod device_type; pub use self::device_type::*;
|
||||||
|
mod device_impl;
|
||||||
|
|
||||||
pub(crate) use ::{
|
pub(crate) use ::{
|
||||||
tek_engine::*,
|
tek_engine::*,
|
||||||
|
|
@ -53,25 +61,56 @@ macro_rules! def_sizes_iter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*;
|
|
||||||
#[cfg(feature = "browse")] mod browse; #[cfg(feature = "browse")] pub use self::browse::*;
|
pub fn view_transport (
|
||||||
#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*;
|
play: bool,
|
||||||
#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*;
|
bpm: Arc<RwLock<String>>,
|
||||||
#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*;
|
beat: Arc<RwLock<String>>,
|
||||||
#[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*;
|
time: Arc<RwLock<String>>,
|
||||||
#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*;
|
) -> impl Content<TuiOut> {
|
||||||
#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*;
|
let theme = ItemTheme::G[96];
|
||||||
#[cfg(feature = "mixer")] mod mixer; #[cfg(feature = "mixer")] pub use self::mixer::*;
|
Tui::bg(Black, row!(Bsp::a(
|
||||||
#[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*;
|
Fill::XY(Align::w(button_play_pause(play))),
|
||||||
#[cfg(feature = "port")] mod port; #[cfg(feature = "port")] pub use self::port::*;
|
Fill::XY(Align::e(row!(
|
||||||
#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*;
|
FieldH(theme, "BPM", bpm),
|
||||||
#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*;
|
FieldH(theme, "Beat", beat),
|
||||||
#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*;
|
FieldH(theme, "Time", time),
|
||||||
#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*;
|
)))
|
||||||
#[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*;
|
)))
|
||||||
#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*;
|
}
|
||||||
#[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*;
|
|
||||||
#[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*;
|
pub fn view_status (
|
||||||
|
sel: Option<Arc<str>>,
|
||||||
|
sr: Arc<RwLock<String>>,
|
||||||
|
buf: Arc<RwLock<String>>,
|
||||||
|
lat: Arc<RwLock<String>>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
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 fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
|
||||||
|
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(" ▗▄▖ ", " ▝▀▘ ",))))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||||
|
|
@ -113,27 +152,6 @@ impl<T: Has<Vec<Device>>> HasDevices for T {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HasDevices {
|
|
||||||
fn devices (&self) -> &Vec<Device>;
|
|
||||||
fn devices_mut (&mut self) -> &mut Vec<Device>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Device {
|
|
||||||
#[cfg(feature = "sampler")]
|
|
||||||
Sampler(Sampler),
|
|
||||||
#[cfg(feature = "lv2")] // TODO
|
|
||||||
Lv2(Lv2),
|
|
||||||
#[cfg(feature = "vst2")] // TODO
|
|
||||||
Vst2,
|
|
||||||
#[cfg(feature = "vst3")] // TODO
|
|
||||||
Vst3,
|
|
||||||
#[cfg(feature = "clap")] // TODO
|
|
||||||
Clap,
|
|
||||||
#[cfg(feature = "sf2")] // TODO
|
|
||||||
Sf2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Device {
|
impl Device {
|
||||||
pub fn name (&self) -> &str {
|
pub fn name (&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -167,11 +185,11 @@ impl Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
|
||||||
|
|
||||||
audio!(|self: DeviceAudio<'a>, client, scope|{
|
audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||||
use Device::*;
|
use Device::*;
|
||||||
match self.0 {
|
match self.0 {
|
||||||
|
Mute => { Control::Continue },
|
||||||
|
Bypass => { /*TODO*/ Control::Continue },
|
||||||
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
||||||
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
||||||
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
||||||
|
|
@ -181,6 +199,622 @@ audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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<u32> } => {
|
||||||
|
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
||||||
|
Pause { position: Option<u32> } => {
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
});
|
||||||
def_command!(DeviceCommand: |device: Device| {});
|
def_command!(DeviceCommand: |device: Device| {});
|
||||||
|
def_command!(ClipCommand: |clip: MidiClip| {
|
||||||
|
SetColor { color: Option<ItemTheme> } => {
|
||||||
|
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||||||
|
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||||||
|
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
SetLoop { looping: Option<bool> } => {
|
||||||
|
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
def_command!(BrowseCommand: |browse: Browse| {
|
||||||
|
SetVisible => Ok(None),
|
||||||
|
SetPath { address: PathBuf } => Ok(None),
|
||||||
|
SetSearch { filter: Arc<str> } => Ok(None),
|
||||||
|
SetCursor { cursor: usize } => Ok(None),
|
||||||
|
});
|
||||||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
|
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
pub fn view_track_row_section (
|
||||||
|
_theme: ItemTheme,
|
||||||
|
button: impl Content<TuiOut>,
|
||||||
|
button_add: impl Content<TuiOut>,
|
||||||
|
content: impl Content<TuiOut>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))),
|
||||||
|
Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content))))
|
||||||
|
}
|
||||||
|
pub fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▐")));
|
||||||
|
let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▌")));
|
||||||
|
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_clip {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def_command!(MidiEditCommand: |editor: MidiEditor| {
|
||||||
|
Show { clip: Option<Arc<RwLock<MidiClip>>> } => {
|
||||||
|
editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) },
|
||||||
|
DeleteNote => {
|
||||||
|
editor.redraw(); todo!() },
|
||||||
|
AppendNote { advance: bool } => {
|
||||||
|
editor.put_note(*advance); editor.redraw(); Ok(None) },
|
||||||
|
SetNotePos { pos: usize } => {
|
||||||
|
editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) },
|
||||||
|
SetNoteLen { len: usize } => {
|
||||||
|
editor.set_note_len(*len); editor.redraw(); Ok(None) },
|
||||||
|
SetNoteScroll { scroll: usize } => {
|
||||||
|
editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) },
|
||||||
|
SetTimePos { pos: usize } => {
|
||||||
|
editor.set_time_pos(*pos); editor.redraw(); Ok(None) },
|
||||||
|
SetTimeScroll { scroll: usize } => {
|
||||||
|
editor.set_time_start(*scroll); editor.redraw(); Ok(None) },
|
||||||
|
SetTimeZoom { zoom: usize } => {
|
||||||
|
editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) },
|
||||||
|
SetTimeLock { lock: bool } => {
|
||||||
|
editor.set_time_lock(*lock); editor.redraw(); Ok(None) },
|
||||||
|
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||||
|
});
|
||||||
|
|
||||||
|
fn to_key (note: usize) -> &'static str {
|
||||||
|
match note % 12 {
|
||||||
|
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
|
||||||
|
10 | 8 | 6 | 3 | 1 => " ",
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
|
||||||
|
-> impl Iterator<Item=(usize, u16, usize)>
|
||||||
|
{
|
||||||
|
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) {
|
||||||
|
let style = Style::default().gray();
|
||||||
|
let label1 = format!(" {}", state.name);
|
||||||
|
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||||
|
if let Some(ref path) = state.path {
|
||||||
|
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||||
|
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||||
|
}
|
||||||
|
//Ok(Rect { x, y, width: w, height: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(feature = "lv2_gui")]
|
||||||
|
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||||||
|
Ok(spawn(move||{
|
||||||
|
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||||||
|
event_loop.set_control_flow(ControlFlow::Wait);
|
||||||
|
event_loop.run_app(&mut ui).unwrap()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "lv2_gui")]
|
||||||
|
fn lv2_ui_instantiate (kind: &str) {
|
||||||
|
//let host = Suil
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_summing <const N: usize> (
|
||||||
|
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||||
|
) -> bool {
|
||||||
|
let channels = buffer.len();
|
||||||
|
for index in 0..frames {
|
||||||
|
if let Some(frame) = next() {
|
||||||
|
for (channel, sample) in frame.iter().enumerate() {
|
||||||
|
let channel = channel % channels;
|
||||||
|
buffer[channel][index] += sample * gain;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_average <const N: usize> (
|
||||||
|
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||||
|
) -> bool {
|
||||||
|
let channels = buffer.len();
|
||||||
|
for index in 0..frames {
|
||||||
|
if let Some(frame) = next() {
|
||||||
|
for (channel, sample) in frame.iter().enumerate() {
|
||||||
|
let channel = channel % channels;
|
||||||
|
let value = buffer[channel][index];
|
||||||
|
buffer[channel][index] = (value + sample * gain) / 2.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||||||
|
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||||||
|
let count = samples.len() as f32;
|
||||||
|
10. * (total / count).log10()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn to_rms (samples: &[f32]) -> f32 {
|
||||||
|
let sum = samples.iter()
|
||||||
|
.map(|s|*s)
|
||||||
|
.reduce(|sum, sample|sum + sample.abs())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
(sum / samples.len() as f32).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
|
||||||
|
col!(
|
||||||
|
FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
|
||||||
|
Fixed::XY(if value >= 0.0 { 13 }
|
||||||
|
else if value >= -1.0 { 12 }
|
||||||
|
else if value >= -2.0 { 11 }
|
||||||
|
else if value >= -3.0 { 10 }
|
||||||
|
else if value >= -4.0 { 9 }
|
||||||
|
else if value >= -6.0 { 8 }
|
||||||
|
else if value >= -9.0 { 7 }
|
||||||
|
else if value >= -12.0 { 6 }
|
||||||
|
else if value >= -15.0 { 5 }
|
||||||
|
else if value >= -20.0 { 4 }
|
||||||
|
else if value >= -25.0 { 3 }
|
||||||
|
else if value >= -30.0 { 2 }
|
||||||
|
else if value >= -40.0 { 1 }
|
||||||
|
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
|
||||||
|
else if value >= -3.0 { Yellow }
|
||||||
|
else { Green }, ())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let left = format!("L/{:>+9.3}", values[0]);
|
||||||
|
let right = format!("R/{:>+9.3}", values[1]);
|
||||||
|
Bsp::s(left, right)
|
||||||
|
}
|
||||||
|
def_command!(PoolCommand: |pool: Pool| {
|
||||||
|
// Toggle visibility of pool
|
||||||
|
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
|
||||||
|
// Select a clip from the clip pool
|
||||||
|
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
|
||||||
|
// Update the contents of the clip pool
|
||||||
|
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
|
||||||
|
// Rename a clip
|
||||||
|
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
|
||||||
|
// Change the length of a clip
|
||||||
|
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
|
||||||
|
// Import from file
|
||||||
|
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||||||
|
command.delegate(browse, |command|Self::Import{command})?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
// Export to file
|
||||||
|
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||||||
|
command.delegate(browse, |command|Self::Export{command})?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
def_command!(PoolClipCommand: |pool: Pool| {
|
||||||
|
Delete { index: usize } => {
|
||||||
|
let index = *index;
|
||||||
|
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
||||||
|
Ok(Some(Self::Add { index, clip }))
|
||||||
|
},
|
||||||
|
Swap { index: usize, other: usize } => {
|
||||||
|
let index = *index;
|
||||||
|
let other = *other;
|
||||||
|
pool.clips_mut().swap(index, other);
|
||||||
|
Ok(Some(Self::Swap { index, other }))
|
||||||
|
},
|
||||||
|
Export { index: usize, path: PathBuf } => {
|
||||||
|
todo!("export clip to midi file");
|
||||||
|
},
|
||||||
|
Add { index: usize, clip: MidiClip } => {
|
||||||
|
let index = *index;
|
||||||
|
let mut index = index;
|
||||||
|
let clip = Arc::new(RwLock::new(clip.clone()));
|
||||||
|
let mut clips = pool.clips_mut();
|
||||||
|
if index >= clips.len() {
|
||||||
|
index = clips.len();
|
||||||
|
clips.push(clip)
|
||||||
|
} else {
|
||||||
|
clips.insert(index, clip);
|
||||||
|
}
|
||||||
|
Ok(Some(Self::Delete { index }))
|
||||||
|
},
|
||||||
|
Import { index: usize, path: PathBuf } => {
|
||||||
|
let index = *index;
|
||||||
|
let bytes = std::fs::read(&path)?;
|
||||||
|
let smf = Smf::parse(bytes.as_slice())?;
|
||||||
|
let mut t = 0u32;
|
||||||
|
let mut events = vec![];
|
||||||
|
for track in smf.tracks.iter() {
|
||||||
|
for event in track.iter() {
|
||||||
|
t += event.delta.as_int();
|
||||||
|
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||||
|
events.push((t, channel.as_int(), message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||||
|
for event in events.iter() {
|
||||||
|
clip.notes[event.0 as usize].push(event.2);
|
||||||
|
}
|
||||||
|
Ok(Self::Add { index, clip }.execute(pool)?)
|
||||||
|
},
|
||||||
|
SetName { index: usize, name: Arc<str> } => {
|
||||||
|
let index = *index;
|
||||||
|
let clip = &mut pool.clips_mut()[index];
|
||||||
|
let old_name = clip.read().unwrap().name.clone();
|
||||||
|
clip.write().unwrap().name = name.clone();
|
||||||
|
Ok(Some(Self::SetName { index, name: old_name }))
|
||||||
|
},
|
||||||
|
SetLength { index: usize, length: usize } => {
|
||||||
|
let index = *index;
|
||||||
|
let clip = &mut pool.clips_mut()[index];
|
||||||
|
let old_len = clip.read().unwrap().length;
|
||||||
|
clip.write().unwrap().length = *length;
|
||||||
|
Ok(Some(Self::SetLength { index, length: old_len }))
|
||||||
|
},
|
||||||
|
SetColor { index: usize, color: ItemColor } => {
|
||||||
|
let index = *index;
|
||||||
|
let mut color = ItemTheme::from(*color);
|
||||||
|
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
||||||
|
Ok(Some(Self::SetColor { index, color: color.base }))
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(RenameCommand: |pool: Pool| {
|
||||||
|
Begin => unreachable!(),
|
||||||
|
Cancel => {
|
||||||
|
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||||
|
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Confirm => {
|
||||||
|
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||||
|
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Set { value: Arc<str> } => {
|
||||||
|
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
|
||||||
|
pool.clips()[clip].write().unwrap().name = value.clone();
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
def_command!(CropCommand: |pool: Pool| {
|
||||||
|
Begin => unreachable!(),
|
||||||
|
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
|
||||||
|
Set { length: usize } => {
|
||||||
|
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
||||||
|
= pool.mode_mut().clone()
|
||||||
|
{
|
||||||
|
let old_length;
|
||||||
|
{
|
||||||
|
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
||||||
|
old_length = Some(clip.read().unwrap().length);
|
||||||
|
clip.write().unwrap().length = *length;
|
||||||
|
}
|
||||||
|
*pool.mode_mut() = None;
|
||||||
|
return Ok(old_length.map(|length|Self::Set { length }))
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Next => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
|
||||||
|
},
|
||||||
|
Prev => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
|
||||||
|
},
|
||||||
|
Inc => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||||||
|
match focus {
|
||||||
|
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||||||
|
ClipLengthFocus::Beat => { *length += PPQ },
|
||||||
|
ClipLengthFocus::Tick => { *length += 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Dec => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||||||
|
match focus {
|
||||||
|
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||||
|
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
||||||
|
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
def_command!(MidiInputCommand: |port: MidiInput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { midi_out: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { midi_in: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_command!(AudioInputCommand: |port: AudioInput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { audio_out: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { audio_in: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_sizes_iter!(InputsSizes => MidiInput);
|
||||||
|
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||||||
|
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||||
|
def_sizes_iter!(ScenesSizes => Scene);
|
||||||
|
|
||||||
|
fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
|
||||||
|
let sample = sample.unwrap().read().unwrap();
|
||||||
|
let theme = sample.color;
|
||||||
|
to.place(&row!(
|
||||||
|
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||||
|
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||||
|
FieldH(theme, "Start", format!("{:<8}", sample.start)),
|
||||||
|
FieldH(theme, "End", format!("{:<8}", sample.end)),
|
||||||
|
FieldH(theme, "Trans", "0"),
|
||||||
|
FieldH(theme, "Gain", format!("{}", sample.gain)),
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
|
||||||
|
let sample = sample.unwrap().read().unwrap();
|
||||||
|
let theme = sample.color;
|
||||||
|
to.place(&Fixed::X(20, col!(
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Trans ", "0"))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))),
|
||||||
|
)))
|
||||||
|
}), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!(
|
||||||
|
Tui::bold(true, "× No sample."),
|
||||||
|
"[r] record",
|
||||||
|
"[Shift-F9] import",
|
||||||
|
)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> {
|
||||||
|
Tui::bold(true, Tui::fg(Tui::g(224), sample
|
||||||
|
.map(|sample|{
|
||||||
|
let sample = sample.read().unwrap();
|
||||||
|
format!("Sample {}-{}", sample.start, sample.end)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(||"No sample".to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_sample (
|
||||||
|
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||||
|
) -> Usually<usize> {
|
||||||
|
let style = if focus { Style::default().green() } else { Style::default() };
|
||||||
|
if focus {
|
||||||
|
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||||
|
}
|
||||||
|
let label1 = format!("{:3} {:12}",
|
||||||
|
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||||
|
sample.name);
|
||||||
|
let label2 = format!("{:>6} {:>6} +0.0",
|
||||||
|
sample.start,
|
||||||
|
sample.end);
|
||||||
|
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||||
|
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||||
|
Ok(label1.len() + label2.len() + 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||||
|
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||||
|
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||||
|
let entry = entry.expect("failed to read drectory entry");
|
||||||
|
let meta = entry.metadata().expect("failed to read entry metadata");
|
||||||
|
if meta.is_file() {
|
||||||
|
files.push(entry.file_name());
|
||||||
|
} else if meta.is_dir() {
|
||||||
|
subdirs.push(entry.file_name());
|
||||||
|
}
|
||||||
|
(subdirs, files)
|
||||||
|
});
|
||||||
|
subdirs.sort();
|
||||||
|
files.sort();
|
||||||
|
Ok((subdirs, files))
|
||||||
|
}
|
||||||
|
|
||||||
|
def_command!(SamplerCommand: |sampler: Sampler| {
|
||||||
|
RecordToggle { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||||
|
let _ = Self::RecordFinish.execute(sampler)?;
|
||||||
|
// autoslice: continue recording at next slot
|
||||||
|
if recording != Some(slot) {
|
||||||
|
Self::RecordBegin { slot }.execute(sampler)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RecordBegin { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
sampler.recording = Some((
|
||||||
|
slot,
|
||||||
|
Some(Arc::new(RwLock::new(Sample::new(
|
||||||
|
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
|
||||||
|
))))
|
||||||
|
));
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
RecordFinish => {
|
||||||
|
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
|
||||||
|
std::mem::swap(sample, &mut sampler.mapped[*index]);
|
||||||
|
sample
|
||||||
|
}); // TODO: undo
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
RecordCancel => {
|
||||||
|
sampler.recording = None;
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
PlaySample { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
if let Some(ref sample) = sampler.mapped[slot] {
|
||||||
|
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
StopSample { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
todo!();
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(FileBrowserCommand: |sampler: Sampler|{
|
||||||
|
//("begin" [] Some(Self::Begin))
|
||||||
|
//("cancel" [] Some(Self::Cancel))
|
||||||
|
//("confirm" [] Some(Self::Confirm))
|
||||||
|
//("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||||
|
//("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
||||||
|
//("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter")))))
|
||||||
|
});
|
||||||
|
def_command!(SceneCommand: |scene: Scene| {
|
||||||
|
SetSize { size: usize } => { todo!() },
|
||||||
|
SetZoom { size: usize } => { todo!() },
|
||||||
|
SetName { name: Arc<str> } =>
|
||||||
|
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||||||
|
SetColor { color: ItemTheme } =>
|
||||||
|
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||||||
|
});
|
||||||
|
def_sizes_iter!(TracksSizes => Track);
|
||||||
|
def_command!(TrackCommand: |track: Track| {
|
||||||
|
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||||||
|
SetMute { mute: Option<bool> } => todo!(),
|
||||||
|
SetSolo { solo: Option<bool> } => todo!(),
|
||||||
|
SetSize { size: usize } => todo!(),
|
||||||
|
SetZoom { zoom: usize } => todo!(),
|
||||||
|
SetName { name: Arc<str> } =>
|
||||||
|
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||||||
|
SetColor { color: ItemTheme } =>
|
||||||
|
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||||||
|
SetRec { rec: Option<bool> } =>
|
||||||
|
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||||||
|
SetMon { mon: Option<bool> } =>
|
||||||
|
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||||||
|
track.width as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_header (theme: ItemTheme, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||||
|
-> impl Content<TuiOut> + use<'a, T>
|
||||||
|
{
|
||||||
|
let ins = ports.len() as u16;
|
||||||
|
let frame = Outer(true, Style::default().fg(Tui::g(96)));
|
||||||
|
let iter = move||ports.iter();
|
||||||
|
let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name()))));
|
||||||
|
let field = FieldV(theme, title, names);
|
||||||
|
Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
Align::x(Tui::bg(Reset, 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))))})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
||||||
|
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
Map::new(iter, move|(
|
||||||
|
_index, name, connections, y, y2
|
||||||
|
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
|
||||||
|
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||||
|
Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ", name))))),
|
||||||
|
Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1,
|
||||||
|
Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
||||||
|
&connect.info)))))))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "vst2")]
|
||||||
|
fn set_vst_plugin <E: Engine> (
|
||||||
|
host: &Arc<Mutex<Plugin<E>>>, _path: &str
|
||||||
|
) -> Usually<PluginKind> {
|
||||||
|
let mut loader = ::vst::host::PluginLoader::load(
|
||||||
|
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||||
|
host.clone()
|
||||||
|
)?;
|
||||||
|
Ok(PluginKind::VST2 {
|
||||||
|
instance: loader.instance()?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
3233
device/device_impl.rs
Normal file
3233
device/device_impl.rs
Normal file
File diff suppressed because it is too large
Load diff
633
device/device_struct.rs
Normal file
633
device/device_struct.rs
Normal file
|
|
@ -0,0 +1,633 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// The source of time.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clock = tek_device::Clock::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)] pub struct Clock {
|
||||||
|
/// JACK transport handle.
|
||||||
|
pub transport: Arc<Option<Transport>>,
|
||||||
|
/// Global temporal resolution (shared by [Moment] fields)
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// Current global sample and usec (monotonic from JACK clock)
|
||||||
|
pub global: Arc<Moment>,
|
||||||
|
/// Global sample and usec at which playback started
|
||||||
|
pub started: Arc<RwLock<Option<Moment>>>,
|
||||||
|
/// Playback offset (when playing not from start)
|
||||||
|
pub offset: Arc<Moment>,
|
||||||
|
/// Current playhead position
|
||||||
|
pub playhead: Arc<Moment>,
|
||||||
|
/// Note quantization factor
|
||||||
|
pub quant: Arc<Quantize>,
|
||||||
|
/// Launch quantization factor
|
||||||
|
pub sync: Arc<LaunchSync>,
|
||||||
|
/// Size of buffer in samples
|
||||||
|
pub chunk: Arc<AtomicUsize>,
|
||||||
|
// Cache of formatted strings
|
||||||
|
pub view_cache: Arc<RwLock<ClockView>>,
|
||||||
|
/// For syncing the clock to an external source
|
||||||
|
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
||||||
|
/// For syncing other devices to this clock
|
||||||
|
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
||||||
|
/// For emitting a metronome
|
||||||
|
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains memoized renders of clock values.
|
||||||
|
///
|
||||||
|
/// Performance optimization.
|
||||||
|
#[derive(Debug)] pub struct ClockView {
|
||||||
|
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||||
|
pub buf: Memo<Option<f64>, String>,
|
||||||
|
pub lat: Memo<Option<f64>, String>,
|
||||||
|
pub bpm: Memo<Option<f64>, String>,
|
||||||
|
pub beat: Memo<Option<f64>, String>,
|
||||||
|
pub time: Memo<Option<f64>, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arranger.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let arranger = tek_device::Arrangement::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)] pub struct Arrangement {
|
||||||
|
/// Project name.
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Base color.
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// JACK client handle.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// FIXME a render of the project arrangement, redrawn on update.
|
||||||
|
/// TODO rename to "render_cache" or smth
|
||||||
|
pub arranger: Arc<RwLock<Buffer>>,
|
||||||
|
/// Display size
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// Display size of clips area
|
||||||
|
pub size_inner: Measure<TuiOut>,
|
||||||
|
/// Source of time
|
||||||
|
#[cfg(feature = "clock")] pub clock: Clock,
|
||||||
|
/// Allows one MIDI clip to be edited
|
||||||
|
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||||
|
/// List of global midi inputs
|
||||||
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||||
|
/// List of global midi outputs
|
||||||
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||||
|
/// List of global audio inputs
|
||||||
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||||
|
/// List of global audio outputs
|
||||||
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||||
|
/// Selected UI element
|
||||||
|
#[cfg(feature = "select")] pub selection: Selection,
|
||||||
|
/// Last track number (to avoid duplicate port names)
|
||||||
|
#[cfg(feature = "track")] pub track_last: usize,
|
||||||
|
/// List of tracks
|
||||||
|
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||||
|
/// Scroll offset of tracks
|
||||||
|
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||||
|
/// List of scenes
|
||||||
|
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||||
|
/// Scroll offset of scenes
|
||||||
|
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browses for phrase to import/export
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let browser = tek_device::Browser::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct Browse {
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
pub dirs: Vec<(OsString, String)>,
|
||||||
|
pub files: Vec<(OsString, String)>,
|
||||||
|
pub filter: String,
|
||||||
|
pub index: usize,
|
||||||
|
pub scroll: usize,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)] pub(crate) struct EntriesIterator<'a> {
|
||||||
|
pub browser: &'a Browse,
|
||||||
|
pub offset: usize,
|
||||||
|
pub length: usize,
|
||||||
|
pub index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)] pub enum BrowseTarget {
|
||||||
|
SaveProject,
|
||||||
|
LoadProject,
|
||||||
|
ImportSample(Arc<RwLock<Option<Sample>>>),
|
||||||
|
ExportSample(Arc<RwLock<Option<Sample>>>),
|
||||||
|
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||||
|
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A MIDI sequence.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clip = tek_device::MidiClip::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)] pub struct MidiClip {
|
||||||
|
pub uuid: uuid::Uuid,
|
||||||
|
/// Name of clip
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Temporal resolution in pulses per quarter note
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Length of clip in pulses
|
||||||
|
pub length: usize,
|
||||||
|
/// Notes in clip
|
||||||
|
pub notes: MidiData,
|
||||||
|
/// Whether to loop the clip or play it once
|
||||||
|
pub looped: bool,
|
||||||
|
/// Start of loop
|
||||||
|
pub loop_start: usize,
|
||||||
|
/// Length of loop
|
||||||
|
pub loop_length: usize,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool,
|
||||||
|
/// Identifying color of clip
|
||||||
|
pub color: ItemTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device that can be plugged into the chain.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let device = tek_engie::Device::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub enum Device {
|
||||||
|
#[default]
|
||||||
|
Bypass,
|
||||||
|
Mute,
|
||||||
|
#[cfg(feature = "sampler")]
|
||||||
|
Sampler(Sampler),
|
||||||
|
#[cfg(feature = "lv2")] // TODO
|
||||||
|
Lv2(Lv2),
|
||||||
|
#[cfg(feature = "vst2")] // TODO
|
||||||
|
Vst2,
|
||||||
|
#[cfg(feature = "vst3")] // TODO
|
||||||
|
Vst3,
|
||||||
|
#[cfg(feature = "clap")] // TODO
|
||||||
|
Clap,
|
||||||
|
#[cfg(feature = "sf2")] // TODO
|
||||||
|
Sf2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some sort of wrapper?
|
||||||
|
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||||
|
|
||||||
|
/// Contains state for viewing and editing a clip.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut editor = tek_device::MidiEditor {
|
||||||
|
/// mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
||||||
|
/// size: Default::default(),
|
||||||
|
/// //keys: Default::default(),
|
||||||
|
/// };
|
||||||
|
/// let _ = editor.put_note(true);
|
||||||
|
/// let _ = editor.put_note(false);
|
||||||
|
/// let _ = editor.clip_status();
|
||||||
|
/// let _ = editor.edit_status();
|
||||||
|
/// ```
|
||||||
|
pub struct MidiEditor {
|
||||||
|
/// Size of editor on screen
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// View mode and state of editor
|
||||||
|
pub mode: PianoHorizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clip, rendered as a horizontal piano roll.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let piano = tek_device::PianoHorizontal::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)] pub struct PianoHorizontal {
|
||||||
|
pub clip: Option<Arc<RwLock<MidiClip>>>,
|
||||||
|
/// Buffer where the whole clip is rerendered on change
|
||||||
|
pub buffer: Arc<RwLock<BigBuffer>>,
|
||||||
|
/// Size of actual notes area
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// The display window
|
||||||
|
pub range: MidiSelection,
|
||||||
|
/// The note cursor
|
||||||
|
pub point: MidiCursor,
|
||||||
|
/// The highlight color palette
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Width of the keyboard
|
||||||
|
pub keys_width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 12 piano keys, some highlighted.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let keys = tek_device::OctaveVertical::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Copy, Clone)] pub struct OctaveVertical {
|
||||||
|
pub on: [bool; 12],
|
||||||
|
pub colors: [Color; 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A LV2 plugin.
|
||||||
|
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub path: Option<Arc<str>>,
|
||||||
|
pub selected: usize,
|
||||||
|
pub mapping: bool,
|
||||||
|
pub midi_ins: Vec<Port<MidiIn>>,
|
||||||
|
pub midi_outs: Vec<Port<MidiOut>>,
|
||||||
|
pub audio_ins: Vec<Port<AudioIn>>,
|
||||||
|
pub audio_outs: Vec<Port<AudioOut>>,
|
||||||
|
|
||||||
|
pub lv2_world: livi::World,
|
||||||
|
pub lv2_instance: livi::Instance,
|
||||||
|
pub lv2_plugin: livi::Plugin,
|
||||||
|
pub lv2_features: Arc<livi::Features>,
|
||||||
|
pub lv2_port_list: Vec<livi::Port>,
|
||||||
|
pub lv2_input_buffer: Vec<livi::event::LV2AtomSequence>,
|
||||||
|
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A LV2 plugin's X11 UI.
|
||||||
|
#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI {
|
||||||
|
pub window: Option<Window>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||||
|
#[default] Rms,
|
||||||
|
Log10,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Log10Meter(pub f32);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct RmsMeter(pub f32);
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum MixingMode {
|
||||||
|
#[default] Summing,
|
||||||
|
Average,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clip pool.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let pool = tek_device::Pool::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)] pub struct Pool {
|
||||||
|
pub visible: bool,
|
||||||
|
/// Selected clip
|
||||||
|
pub clip: AtomicUsize,
|
||||||
|
/// Mode switch
|
||||||
|
pub mode: Option<PoolMode>,
|
||||||
|
/// Embedded file browse
|
||||||
|
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
||||||
|
/// Collection of MIDI clips.
|
||||||
|
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||||
|
/// Collection of sound samples.
|
||||||
|
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays and edits clip length.
|
||||||
|
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
||||||
|
/// Pulses per beat (quaver)
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Beats per bar
|
||||||
|
pub bpb: usize,
|
||||||
|
/// Length of clip in pulses
|
||||||
|
pub pulses: usize,
|
||||||
|
/// Selected subdivision
|
||||||
|
pub focus: Option<ClipLengthFocus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some sort of wrapper again?
|
||||||
|
pub struct PoolView<'a>(pub &'a Pool);
|
||||||
|
|
||||||
|
/// Audio input port.
|
||||||
|
#[derive(Debug)] pub struct AudioInput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<AudioIn>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Audio output port.
|
||||||
|
#[derive(Debug)] pub struct AudioOutput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<AudioOut>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI input port.
|
||||||
|
#[derive(Debug)] pub struct MidiInput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<MidiIn>,
|
||||||
|
/// List of currently held notes.
|
||||||
|
pub held: Arc<RwLock<[bool;128]>>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI output port.
|
||||||
|
#[derive(Debug)] pub struct MidiOutput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<MidiOut>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
/// List of currently held notes.
|
||||||
|
pub held: Arc<RwLock<[bool;128]>>,
|
||||||
|
/// Buffer
|
||||||
|
pub note_buffer: Vec<u8>,
|
||||||
|
/// Buffer
|
||||||
|
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port connection manager.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let connect = tek_engine::Connect::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, Default)] pub struct Connect {
|
||||||
|
pub name: Option<ConnectName>,
|
||||||
|
pub scope: Option<ConnectScope>,
|
||||||
|
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||||
|
pub info: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays [Voice]s from [Sample]s.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let sampler = tek_device::Sampler::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Sampler {
|
||||||
|
/// Name of sampler.
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Device color.
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Audio input ports. Samples get recorded here.
|
||||||
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||||
|
/// Audio input meters.
|
||||||
|
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
|
||||||
|
/// Sample currently being recorded.
|
||||||
|
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
|
||||||
|
/// Recording buffer.
|
||||||
|
pub buffer: Vec<Vec<f32>>,
|
||||||
|
/// Samples mapped to MIDI notes.
|
||||||
|
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
||||||
|
/// Samples that are not mapped to MIDI notes.
|
||||||
|
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||||
|
/// Sample currently being edited.
|
||||||
|
pub editing: Option<Arc<RwLock<Sample>>>,
|
||||||
|
/// MIDI input port. Triggers sample playback.
|
||||||
|
#[cfg(feature = "port")] pub midi_in: Option<MidiInput>,
|
||||||
|
/// Collection of currently playing instances of samples.
|
||||||
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
|
/// Audio output ports. Voices get played here.
|
||||||
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||||
|
/// Audio output meters.
|
||||||
|
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
||||||
|
/// How to mix the voices.
|
||||||
|
pub mixing_mode: MixingMode,
|
||||||
|
/// How to meter the inputs and outputs.
|
||||||
|
pub metering_mode: MeteringMode,
|
||||||
|
/// Fixed gain applied to all output.
|
||||||
|
pub output_gain: f32,
|
||||||
|
/// Currently active modal, if any.
|
||||||
|
pub mode: Option<SamplerMode>,
|
||||||
|
/// Size of rendered sampler.
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// Lowest note displayed.
|
||||||
|
pub note_lo: AtomicUsize,
|
||||||
|
/// Currently selected note.
|
||||||
|
pub note_pt: AtomicUsize,
|
||||||
|
/// Selected note as row/col.
|
||||||
|
pub cursor: (AtomicUsize, AtomicUsize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sound cut.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let sample = tek_device::Sample::default();
|
||||||
|
/// let sample = tek_device::Sample::new("test", 0, 0, vec![]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)] pub struct Sample {
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub channels: Vec<Vec<f32>>,
|
||||||
|
pub rate: Option<usize>,
|
||||||
|
pub gain: f32,
|
||||||
|
pub color: ItemTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A currently playing instance of a sample.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct Voice {
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub after: usize,
|
||||||
|
pub position: usize,
|
||||||
|
pub velocity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddSampleModal {
|
||||||
|
pub exited: bool,
|
||||||
|
pub dir: PathBuf,
|
||||||
|
pub subdirs: Vec<OsString>,
|
||||||
|
pub files: Vec<OsString>,
|
||||||
|
pub cursor: usize,
|
||||||
|
pub offset: usize,
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
|
pub _search: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scene consists of a set of clips to play together.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let scene: tek_device::Scene = Default::default();
|
||||||
|
/// let _ = scene.pulses();
|
||||||
|
/// let _ = scene.is_playing(&[]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Scene {
|
||||||
|
/// Name of scene
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Identifying color of scene
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Clips in scene, one per track
|
||||||
|
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||||
|
pub enum Selection {
|
||||||
|
#[default]
|
||||||
|
/// Nothing is selected
|
||||||
|
Nothing,
|
||||||
|
/// The whole mix is selected
|
||||||
|
Mix,
|
||||||
|
/// A MIDI input is selected.
|
||||||
|
Input(usize),
|
||||||
|
/// A MIDI output is selected.
|
||||||
|
Output(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
#[cfg(feature = "scene")] Scene(usize),
|
||||||
|
/// A track is selected.
|
||||||
|
#[cfg(feature = "track")] Track(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||||||
|
/// A track's MIDI input connection is selected.
|
||||||
|
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||||||
|
/// A track's MIDI output connection is selected.
|
||||||
|
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||||||
|
/// A track device slot is selected.
|
||||||
|
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains state for playing a clip
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clip = tek_device::MidiClip::default();
|
||||||
|
/// println!("Empty clip: {clip:?}");
|
||||||
|
///
|
||||||
|
/// let clip = tek_device::MidiClip::stop_all();
|
||||||
|
/// println!("Panic clip: {clip:?}");
|
||||||
|
///
|
||||||
|
/// let mut clip = tek_device::MidiClip::new("clip", true, 1, None, None);
|
||||||
|
/// clip.set_length(96);
|
||||||
|
/// clip.toggle_loop();
|
||||||
|
/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||||
|
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||||
|
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||||
|
///
|
||||||
|
/// let clip = std::sync::Arc::new(clip);
|
||||||
|
/// assert_eq!(clip.clone(), clip);
|
||||||
|
///
|
||||||
|
/// let sequencer = tek_device::Sequencer::default();
|
||||||
|
/// println!("{sequencer:?}");
|
||||||
|
/// ```
|
||||||
|
pub struct Sequencer {
|
||||||
|
/// State of clock and playhead
|
||||||
|
#[cfg(feature = "clock")] pub clock: Clock,
|
||||||
|
/// Start time and clip being played
|
||||||
|
#[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
|
/// Start time and next clip
|
||||||
|
#[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
|
/// Record from MIDI ports to current sequence.
|
||||||
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||||
|
/// Play from current sequence to MIDI ports
|
||||||
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||||
|
/// Play input through output.
|
||||||
|
pub monitoring: bool,
|
||||||
|
/// Write input to sequence.
|
||||||
|
pub recording: bool,
|
||||||
|
/// Overdub input to sequence.
|
||||||
|
pub overdub: bool,
|
||||||
|
/// Send all notes off
|
||||||
|
pub reset: bool, // TODO?: after Some(nframes)
|
||||||
|
/// Notes currently held at input
|
||||||
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Notes currently held at output
|
||||||
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
pub note_buf: Vec<u8>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track consists of a sequencer and zero or more devices chained after it.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let track: tek_device::Track = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Track {
|
||||||
|
/// Name of track
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Identifying color of track
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Preferred width of track column
|
||||||
|
pub width: usize,
|
||||||
|
/// MIDI sequencer state
|
||||||
|
pub sequencer: Sequencer,
|
||||||
|
/// Device chain
|
||||||
|
pub devices: Vec<Device>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands supported by [Browse]
|
||||||
|
//#[derive(Debug, Clone, PartialEq)]
|
||||||
|
//pub enum BrowseCommand {
|
||||||
|
//Begin,
|
||||||
|
//Cancel,
|
||||||
|
//Confirm,
|
||||||
|
//Select(usize),
|
||||||
|
//Chdir(PathBuf),
|
||||||
|
//Filter(Arc<str>),
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// Modes for clip pool
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PoolMode {
|
||||||
|
/// Renaming a pattern
|
||||||
|
Rename(usize, Arc<str>),
|
||||||
|
/// Editing the length of a pattern
|
||||||
|
Length(usize, usize, ClipLengthFocus),
|
||||||
|
/// Load clip from disk
|
||||||
|
Import(usize, Browse),
|
||||||
|
/// Save clip to disk
|
||||||
|
Export(usize, Browse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focused field of `ClipLength`
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum ClipLengthFocus {
|
||||||
|
/// Editing the number of bars
|
||||||
|
Bar,
|
||||||
|
/// Editing the number of beats
|
||||||
|
Beat,
|
||||||
|
/// Editing the number of ticks
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ConnectName {
|
||||||
|
/** Exact match */
|
||||||
|
Exact(Arc<str>),
|
||||||
|
/** Match regular expression */
|
||||||
|
RegExp(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
||||||
|
One,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
||||||
|
Missing,
|
||||||
|
Disconnected,
|
||||||
|
Connected,
|
||||||
|
Mismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SamplerMode {
|
||||||
|
// Load sample from path
|
||||||
|
Import(usize, Browse),
|
||||||
|
}
|
||||||
794
device/device_trait.rs
Normal file
794
device/device_trait.rs
Normal file
|
|
@ -0,0 +1,794 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait HasSceneScroll: HasScenes {
|
||||||
|
fn scene_scroll (&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasTrackScroll: HasTracks {
|
||||||
|
fn track_scroll (&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasMidiClip {
|
||||||
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasClipsSize {
|
||||||
|
fn clips_size (&self) -> &Measure<TuiOut>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasClock: Send + Sync {
|
||||||
|
fn clock (&self) -> &Clock;
|
||||||
|
fn clock_mut (&mut self) -> &mut Clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasDevices {
|
||||||
|
fn devices (&self) -> &Vec<Device>;
|
||||||
|
fn devices_mut (&mut self) -> &mut Vec<Device>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasSelection: Has<Selection> {
|
||||||
|
fn selection (&self) -> &Selection { self.get() }
|
||||||
|
fn selection_mut (&mut self) -> &mut Selection { self.get_mut() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasWidth {
|
||||||
|
const MIN_WIDTH: usize;
|
||||||
|
/// Increment track width.
|
||||||
|
fn width_inc (&mut self);
|
||||||
|
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||||||
|
fn width_dec (&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasMidiBuffers {
|
||||||
|
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
|
||||||
|
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasSequencer {
|
||||||
|
fn sequencer (&self) -> &Sequencer;
|
||||||
|
fn sequencer_mut (&mut self) -> &mut Sequencer;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
|
||||||
|
fn scene (&self) -> Option<&Scene> {
|
||||||
|
Has::<Option<Scene>>::get(self).as_ref()
|
||||||
|
}
|
||||||
|
fn scene_mut (&mut self) -> &mut Option<Scene> {
|
||||||
|
Has::<Option<Scene>>::get_mut(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
||||||
|
fn scenes (&self) -> &Vec<Scene> {
|
||||||
|
Has::<Vec<Scene>>::get(self)
|
||||||
|
}
|
||||||
|
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
||||||
|
Has::<Vec<Scene>>::get_mut(self)
|
||||||
|
}
|
||||||
|
/// Generate the default name for a new scene
|
||||||
|
fn scene_default_name (&self) -> Arc<str> {
|
||||||
|
format!("s{:3>}", self.scenes().len() + 1).into()
|
||||||
|
}
|
||||||
|
fn scene_longest_name (&self) -> usize {
|
||||||
|
self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ```
|
||||||
|
/// struct TestEditorHost(Option<MidiEditor>);
|
||||||
|
/// has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
||||||
|
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
|
||||||
|
/// let _ = host.editor();
|
||||||
|
/// let _ = host.editor_mut();
|
||||||
|
/// let _ = host.is_editing();
|
||||||
|
/// let _ = host.editor_w();
|
||||||
|
/// let _ = host.editor_h();
|
||||||
|
/// ```
|
||||||
|
pub trait HasEditor: Has<Option<MidiEditor>> {
|
||||||
|
fn editor (&self) -> Option<&MidiEditor> {
|
||||||
|
self.get().as_ref()
|
||||||
|
}
|
||||||
|
fn editor_mut (&mut self) -> Option<&mut MidiEditor> {
|
||||||
|
self.get_mut().as_mut()
|
||||||
|
}
|
||||||
|
fn is_editing (&self) -> bool {
|
||||||
|
self.editor().is_some()
|
||||||
|
}
|
||||||
|
fn editor_w (&self) -> usize {
|
||||||
|
self.editor().map(|e|e.size.w()).unwrap_or(0) as usize
|
||||||
|
}
|
||||||
|
fn editor_h (&self) -> usize {
|
||||||
|
self.editor().map(|e|e.size.h()).unwrap_or(0) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasClips {
|
||||||
|
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||||
|
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||||
|
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||||
|
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||||
|
self.clips_mut().push(clip.clone());
|
||||||
|
(self.clips().len() - 1, clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for thing that may receive MIDI.
|
||||||
|
pub trait HasMidiIns {
|
||||||
|
fn midi_ins (&self) -> &Vec<MidiInput>;
|
||||||
|
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
||||||
|
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
||||||
|
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
||||||
|
self.midi_ins().iter()
|
||||||
|
.map(|port|port.port().iter(scope)
|
||||||
|
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
fn midi_ins_with_sizes <'a> (&'a self) ->
|
||||||
|
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||||
|
{
|
||||||
|
let mut y = 0;
|
||||||
|
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
||||||
|
let height = 1 + input.connections().len();
|
||||||
|
let data = (i, input.port_name(), input.connections(), y, y + height);
|
||||||
|
y += height;
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for thing that may output MIDI.
|
||||||
|
pub trait HasMidiOuts {
|
||||||
|
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
||||||
|
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
||||||
|
fn midi_outs_with_sizes <'a> (&'a self) ->
|
||||||
|
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||||
|
{
|
||||||
|
let mut y = 0;
|
||||||
|
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
||||||
|
let height = 1 + output.connections().len();
|
||||||
|
let data = (i, output.port_name(), output.connections(), y, y + height);
|
||||||
|
y += height;
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
||||||
|
for port in self.midi_outs_mut().iter_mut() {
|
||||||
|
port.buffer_emit(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasTracks: Has<Vec<Track>> + Send + Sync {
|
||||||
|
fn tracks (&self) -> &Vec<Track> { Has::<Vec<Track>>::get(self) }
|
||||||
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { Has::<Vec<Track>>::get_mut(self) }
|
||||||
|
/// Run audio callbacks for every track and every device
|
||||||
|
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
for track in self.tracks_mut().iter_mut() {
|
||||||
|
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
for device in track.devices.iter_mut() {
|
||||||
|
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||||||
|
/// Stop all playing clips
|
||||||
|
fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } }
|
||||||
|
/// Stop all playing clips
|
||||||
|
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
||||||
|
if let Some(clips) = clips {
|
||||||
|
for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); }
|
||||||
|
} else {
|
||||||
|
for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Spacing between tracks.
|
||||||
|
const TRACK_SPACING: usize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasTrack {
|
||||||
|
fn track (&self) -> Option<&Track>;
|
||||||
|
fn track_mut (&mut self) -> Option<&mut Track>;
|
||||||
|
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + 'a {
|
||||||
|
self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> + '_ {
|
||||||
|
self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||||
|
self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||||
|
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasPlayClip: HasClock {
|
||||||
|
|
||||||
|
fn reset (&self) -> bool;
|
||||||
|
|
||||||
|
fn reset_mut (&mut self) -> &mut bool;
|
||||||
|
|
||||||
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
|
|
||||||
|
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
|
|
||||||
|
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
|
|
||||||
|
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
|
|
||||||
|
fn pulses_since_start (&self) -> Option<f64> {
|
||||||
|
if let Some((started, Some(_))) = self.play_clip().as_ref() {
|
||||||
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||||
|
return Some(elapsed)
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
||||||
|
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
|
||||||
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||||
|
let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
|
||||||
|
let times = (elapsed as usize / length) as f64;
|
||||||
|
let elapsed = (elapsed as usize % length) as f64;
|
||||||
|
return Some((times, elapsed))
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||||
|
*self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned()));
|
||||||
|
*self.reset_mut() = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_status (&self) -> impl Content<TuiOut> {
|
||||||
|
let (name, color): (Arc<str>, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() {
|
||||||
|
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
||||||
|
(name.clone(), color)
|
||||||
|
} else {
|
||||||
|
("".into(), Tui::g(64).into())
|
||||||
|
};
|
||||||
|
let time: String = self.pulses_since_start_looped()
|
||||||
|
.map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time)))
|
||||||
|
.unwrap_or_else(||String::from(" ")).into();
|
||||||
|
FieldV(color, "Now:", format!("{} {}", time, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_status (&self) -> impl Content<TuiOut> {
|
||||||
|
let mut time: Arc<str> = String::from("--.-.--").into();
|
||||||
|
let mut name: Arc<str> = String::from("").into();
|
||||||
|
let mut color = ItemTheme::G[64];
|
||||||
|
let clock = self.clock();
|
||||||
|
if let Some((t, Some(clip))) = self.next_clip() {
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
name = clip.name.clone();
|
||||||
|
color = clip.color.clone();
|
||||||
|
time = {
|
||||||
|
let target = t.pulse.get();
|
||||||
|
let current = clock.playhead.pulse.get();
|
||||||
|
if target > current {
|
||||||
|
let remaining = target - current;
|
||||||
|
format!("-{:>}", clock.timebase.format_beats_1(remaining))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}.into()
|
||||||
|
} else if let Some((t, Some(clip))) = self.play_clip() {
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
if clip.looped {
|
||||||
|
name = clip.name.clone();
|
||||||
|
color = clip.color.clone();
|
||||||
|
let target = t.pulse.get() + clip.length as f64;
|
||||||
|
let current = clock.playhead.pulse.get();
|
||||||
|
if target > current {
|
||||||
|
time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = "Stop".to_string().into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FieldV(color, "Next:", format!("{} {}", time, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiMonitor: HasMidiIns + HasMidiBuffers {
|
||||||
|
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||||
|
fn monitoring (&self) -> bool;
|
||||||
|
fn monitoring_mut (&mut self) -> &mut bool;
|
||||||
|
fn toggle_monitor (&mut self) {
|
||||||
|
*self.monitoring_mut() = !self.monitoring();
|
||||||
|
}
|
||||||
|
fn monitor (&mut self, scope: &ProcessScope) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
|
||||||
|
fn recording (&self) -> bool;
|
||||||
|
fn recording_mut (&mut self) -> &mut bool;
|
||||||
|
fn toggle_record (&mut self) {
|
||||||
|
*self.recording_mut() = !self.recording();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overdub (&self) -> bool;
|
||||||
|
fn overdub_mut (&mut self) -> &mut bool;
|
||||||
|
fn toggle_overdub (&mut self) {
|
||||||
|
*self.overdub_mut() = !self.overdub();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_clip (
|
||||||
|
&mut self,
|
||||||
|
scope: &ProcessScope,
|
||||||
|
started: Moment,
|
||||||
|
clip: &Option<Arc<RwLock<MidiClip>>>,
|
||||||
|
) {
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
let sample0 = scope.last_frame_time() as usize;
|
||||||
|
let start = started.sample.get() as usize;
|
||||||
|
let _recording = self.recording();
|
||||||
|
let timebase = self.clock().timebase().clone();
|
||||||
|
let quant = self.clock().quant.get();
|
||||||
|
let mut clip = clip.write().unwrap();
|
||||||
|
let length = clip.length;
|
||||||
|
for input in self.midi_ins_mut().iter() {
|
||||||
|
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
|
clip.record_event({
|
||||||
|
let sample = (sample0 + sample - start) as f64;
|
||||||
|
let pulse = timebase.samples_to_pulse(sample);
|
||||||
|
let quantized = (pulse / quant).round() * quant;
|
||||||
|
quantized as usize % length
|
||||||
|
}, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_next (&mut self) {
|
||||||
|
// TODO switch to next clip and record into it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiViewer: Measured<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
|
||||||
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
|
||||||
|
fn redraw (&self);
|
||||||
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
||||||
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
||||||
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||||
|
*self.clip_mut() = clip.cloned();
|
||||||
|
self.redraw();
|
||||||
|
}
|
||||||
|
/// Make sure cursor is within note range
|
||||||
|
fn autoscroll (&self) {
|
||||||
|
let note_pos = self.get_note_pos().min(127);
|
||||||
|
let note_lo = self.get_note_lo();
|
||||||
|
let note_hi = self.get_note_hi();
|
||||||
|
if note_pos < note_lo {
|
||||||
|
self.note_lo().set(note_pos);
|
||||||
|
} else if note_pos > note_hi {
|
||||||
|
self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Make sure time range is within display
|
||||||
|
fn autozoom (&self) {
|
||||||
|
if self.time_lock().get() {
|
||||||
|
let time_len = self.get_time_len();
|
||||||
|
let time_axis = self.get_time_axis();
|
||||||
|
let time_zoom = self.get_time_zoom();
|
||||||
|
loop {
|
||||||
|
let time_zoom = self.time_zoom().get();
|
||||||
|
let time_area = time_axis * time_zoom;
|
||||||
|
if time_area > time_len {
|
||||||
|
let next_time_zoom = note_duration_prev(time_zoom);
|
||||||
|
if next_time_zoom <= 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let next_time_area = time_axis * next_time_zoom;
|
||||||
|
if next_time_area >= time_len {
|
||||||
|
self.time_zoom().set(next_time_zoom);
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if time_area < time_len {
|
||||||
|
let prev_time_zoom = note_duration_next(time_zoom);
|
||||||
|
if prev_time_zoom > 384 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let prev_time_area = time_axis * prev_time_zoom;
|
||||||
|
if prev_time_area <= time_len {
|
||||||
|
self.time_zoom().set(prev_time_zoom);
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if time_zoom != self.time_zoom().get() {
|
||||||
|
self.redraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//while time_len.div_ceil(time_zoom) > time_axis {
|
||||||
|
//println!("\r{time_len} {time_zoom} {time_axis}");
|
||||||
|
//time_zoom = Note::next(time_zoom);
|
||||||
|
//}
|
||||||
|
//self.time_zoom().set(time_zoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AddScene: HasScenes + HasTracks {
|
||||||
|
/// Add multiple scenes
|
||||||
|
fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
||||||
|
let scene_color_1 = ItemColor::random();
|
||||||
|
let scene_color_2 = ItemColor::random();
|
||||||
|
for i in 0..n {
|
||||||
|
let _ = self.scene_add(None, Some(
|
||||||
|
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Add a scene
|
||||||
|
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
||||||
|
-> Usually<(usize, &mut Scene)>
|
||||||
|
{
|
||||||
|
let scene = Scene {
|
||||||
|
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||||
|
clips: vec![None;self.tracks().len()],
|
||||||
|
color: color.unwrap_or_else(ItemTheme::random),
|
||||||
|
};
|
||||||
|
self.scenes_mut().push(scene);
|
||||||
|
let index = self.scenes().len() - 1;
|
||||||
|
Ok((index, &mut self.scenes_mut()[index]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ClipsView: TracksView + ScenesView {
|
||||||
|
|
||||||
|
fn view_scenes_clips <'a> (&'a self)
|
||||||
|
-> impl Content<TuiOut> + 'a
|
||||||
|
{
|
||||||
|
self.clips_size().of(Fill::XY(Bsp::a(
|
||||||
|
Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
||||||
|
Thunk::new(|to: &mut TuiOut|for (
|
||||||
|
track_index, track, _, _
|
||||||
|
) in self.tracks_with_sizes() {
|
||||||
|
to.place(&Fixed::X(track.width as u16,
|
||||||
|
Fill::Y(self.view_track_clips(track_index, track))))
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
|
||||||
|
Thunk::new(move|to: &mut TuiOut|for (
|
||||||
|
scene_index, scene, ..
|
||||||
|
) in self.scenes_with_sizes() {
|
||||||
|
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||||
|
} else {
|
||||||
|
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||||
|
};
|
||||||
|
let fg = theme.lightest.rgb;
|
||||||
|
let mut outline = theme.base.rgb;
|
||||||
|
let bg = if self.selection().track() == Some(track_index)
|
||||||
|
&& self.selection().scene() == Some(scene_index)
|
||||||
|
{
|
||||||
|
outline = theme.lighter.rgb;
|
||||||
|
theme.light.rgb
|
||||||
|
} else if self.selection().track() == Some(track_index)
|
||||||
|
|| self.selection().scene() == Some(scene_index)
|
||||||
|
{
|
||||||
|
outline = theme.darkest.rgb;
|
||||||
|
theme.base.rgb
|
||||||
|
} else {
|
||||||
|
theme.dark.rgb
|
||||||
|
};
|
||||||
|
let w = if self.selection().track() == Some(track_index)
|
||||||
|
&& let Some(editor) = self.editor ()
|
||||||
|
{
|
||||||
|
(editor.measure_width() as usize).max(24).max(track.width)
|
||||||
|
} else {
|
||||||
|
track.width
|
||||||
|
} as u16;
|
||||||
|
let y = if self.selection().scene() == Some(scene_index)
|
||||||
|
&& let Some(editor) = self.editor ()
|
||||||
|
{
|
||||||
|
(editor.measure_height() as usize).max(12)
|
||||||
|
} else {
|
||||||
|
Self::H_SCENE as usize
|
||||||
|
} as u16;
|
||||||
|
|
||||||
|
to.place(&Fixed::XY(w, y, Bsp::b(
|
||||||
|
Fill::XY(Outer(true, Style::default().fg(outline))),
|
||||||
|
Fill::XY(Bsp::b(
|
||||||
|
Bsp::b(
|
||||||
|
Tui::fg_bg(outline, bg, Fill::XY("")),
|
||||||
|
Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
||||||
|
),
|
||||||
|
Fill::XY(When::new(self.selection().track() == Some(track_index)
|
||||||
|
&& self.selection().scene() == Some(scene_index)
|
||||||
|
&& self.is_editing(), self.editor())))))));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured<TuiOut> {
|
||||||
|
|
||||||
|
fn tracks_width_available (&self) -> u16 {
|
||||||
|
(self.measure_width() as u16).saturating_sub(40)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over tracks with their corresponding sizes.
|
||||||
|
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||||
|
let _editor_width = self.editor().map(|e|e.measure_width());
|
||||||
|
let _active_track = self.selection().track();
|
||||||
|
let mut x = 0;
|
||||||
|
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||||||
|
let width = track.width.max(8);
|
||||||
|
if x + width < self.clips_size().w() as usize {
|
||||||
|
let data = (index, track, x, x + width);
|
||||||
|
x += width + Self::TRACK_SPACING;
|
||||||
|
Some(data)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||||
|
let track_count = self.tracks().len();
|
||||||
|
let scene_count = self.scenes().len();
|
||||||
|
let selected = self.selection();
|
||||||
|
let button = Bsp::s(
|
||||||
|
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
||||||
|
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
||||||
|
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
||||||
|
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
||||||
|
let button_2 = Bsp::s(
|
||||||
|
button_2("T", "+", false),
|
||||||
|
button_2("S", "+", false));
|
||||||
|
view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb,
|
||||||
|
Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{
|
||||||
|
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track),
|
||||||
|
Tui::bg(if selected.track() == Some(index) {
|
||||||
|
track.color.light.rgb
|
||||||
|
} else {
|
||||||
|
track.color.base.rgb
|
||||||
|
}, Bsp::s(Fill::X(Align::nw(Bsp::e(
|
||||||
|
format!("·t{index:02} "),
|
||||||
|
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name))
|
||||||
|
))), ""))) ));}}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content<TuiOut> {
|
||||||
|
view_track_row_section(theme,
|
||||||
|
Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))),
|
||||||
|
Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() {
|
||||||
|
to.place(&Fill::X(Align::w(port.port_name())));
|
||||||
|
})),
|
||||||
|
button_2("O", "+", false),
|
||||||
|
Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{
|
||||||
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
to.place(&Fixed::X(track_width(index, track),
|
||||||
|
Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(),
|
||||||
|
|port, index|Tui::fg(Rgb(255, 255, 255),
|
||||||
|
Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w(
|
||||||
|
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
||||||
|
let mut h = 0u16;
|
||||||
|
for track in self.tracks().iter() {
|
||||||
|
h = h.max(track.sequencer.midi_ins.len() as u16);
|
||||||
|
}
|
||||||
|
let content = 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,
|
||||||
|
Align::nw(Bsp::s(
|
||||||
|
Tui::bg(track.color.base.rgb,
|
||||||
|
Fill::X(Align::w(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 "),
|
||||||
|
)))),
|
||||||
|
Map::south(1, ||track.sequencer.midi_ins.iter(),
|
||||||
|
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
||||||
|
Fill::X(Align::w(format!("·i{index:02} {}", port.port_name())))))))));
|
||||||
|
});
|
||||||
|
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||||||
|
Tui::bg(theme.darker.rgb, Align::w(content)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||||||
|
|
||||||
|
/// Default scene height.
|
||||||
|
const H_SCENE: usize = 2;
|
||||||
|
|
||||||
|
/// Default editor height.
|
||||||
|
const H_EDITOR: usize = 15;
|
||||||
|
|
||||||
|
fn h_scenes (&self) -> u16;
|
||||||
|
|
||||||
|
fn w_side (&self) -> u16;
|
||||||
|
|
||||||
|
fn w_mid (&self) -> u16;
|
||||||
|
|
||||||
|
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||||||
|
let mut y = 0;
|
||||||
|
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||||||
|
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
||||||
|
8
|
||||||
|
} else {
|
||||||
|
Self::H_SCENE
|
||||||
|
};
|
||||||
|
if y + height <= self.clips_size().h() as usize {
|
||||||
|
let data = (s, scene, y, y + height);
|
||||||
|
y += height;
|
||||||
|
Some(data)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_scenes_names (&self) -> impl Content<TuiOut> {
|
||||||
|
Fixed::X(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() {
|
||||||
|
to.place(&self.view_scene_name(index, scene));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Content<TuiOut> + 'a {
|
||||||
|
let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() {
|
||||||
|
7
|
||||||
|
} else {
|
||||||
|
Self::H_SCENE as u16
|
||||||
|
};
|
||||||
|
let bg = if self.selection().scene() == Some(index) {
|
||||||
|
scene.color.light.rgb
|
||||||
|
} else {
|
||||||
|
scene.color.base.rgb
|
||||||
|
};
|
||||||
|
let a = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "),
|
||||||
|
Tui::fg(Tui::g(255), Tui::bold(true, &scene.name)))));
|
||||||
|
let b = When::new(self.selection().scene() == Some(index) && self.is_editing(),
|
||||||
|
Fill::XY(Align::nw(Bsp::s(
|
||||||
|
self.editor().as_ref().map(|e|e.clip_status()),
|
||||||
|
self.editor().as_ref().map(|e|e.edit_status())))));
|
||||||
|
Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b))))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// May create new MIDI input ports.
|
||||||
|
pub trait AddMidiIn {
|
||||||
|
fn midi_in_add (&mut self) -> Usually<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// May create new MIDI output ports.
|
||||||
|
pub trait AddMidiOut {
|
||||||
|
fn midi_out_add (&mut self) -> Usually<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RegisterPorts: HasJack<'static> {
|
||||||
|
/// Register a MIDI input port.
|
||||||
|
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
||||||
|
/// Register a MIDI output port.
|
||||||
|
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
||||||
|
/// Register an audio input port.
|
||||||
|
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
||||||
|
/// Register an audio output port.
|
||||||
|
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait JackPort: HasJack<'static> {
|
||||||
|
type Port: PortSpec + Default;
|
||||||
|
type Pair: PortSpec + Default;
|
||||||
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
|
-> Usually<Self> where Self: Sized;
|
||||||
|
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||||
|
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
||||||
|
.map_err(|e|e.into())
|
||||||
|
}
|
||||||
|
fn port_name (&self) -> &Arc<str>;
|
||||||
|
fn connections (&self) -> &[Connect];
|
||||||
|
fn port (&self) -> &Port<Self::Port>;
|
||||||
|
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||||
|
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
||||||
|
fn close (self) -> Usually<()> where Self: Sized {
|
||||||
|
let jack = self.jack().clone();
|
||||||
|
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
||||||
|
}
|
||||||
|
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
||||||
|
self.with_client(|c|c.ports(re_name, re_type, flags))
|
||||||
|
}
|
||||||
|
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||||
|
self.with_client(|c|c.port_by_id(id))
|
||||||
|
}
|
||||||
|
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
||||||
|
self.with_client(|c|c.port_by_name(name.as_ref()))
|
||||||
|
}
|
||||||
|
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
||||||
|
for connect in self.connections().iter() {
|
||||||
|
//panic!("{connect:?}");
|
||||||
|
let status = match &connect.name {
|
||||||
|
Exact(name) => self.connect_exact(name),
|
||||||
|
RegExp(re) => self.connect_regexp(re, connect.scope),
|
||||||
|
}?;
|
||||||
|
*connect.status.write().unwrap() = status;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn connect_exact <'k> (&'k self, name: &str) ->
|
||||||
|
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
||||||
|
{
|
||||||
|
self.with_client(move|c|{
|
||||||
|
let mut status = vec![];
|
||||||
|
for port in c.ports(None, None, PortFlags::empty()).iter() {
|
||||||
|
if port.as_str() == &*name {
|
||||||
|
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||||
|
let port_status = self.connect_to_unowned(&port)?;
|
||||||
|
let name = port.name()?.into();
|
||||||
|
status.push((port, name, port_status));
|
||||||
|
if port_status == Connected {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn connect_regexp <'k> (
|
||||||
|
&'k self, re: &str, scope: ConnectScope
|
||||||
|
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
||||||
|
self.with_client(move|c|{
|
||||||
|
let mut status = vec![];
|
||||||
|
let ports = c.ports(Some(&re), None, PortFlags::empty());
|
||||||
|
for port in ports.iter() {
|
||||||
|
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||||
|
let port_status = self.connect_to_unowned(&port)?;
|
||||||
|
let name = port.name()?.into();
|
||||||
|
status.push((port, name, port_status));
|
||||||
|
if port_status == Connected && scope == One {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** Connect to a matching port by name. */
|
||||||
|
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
||||||
|
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
||||||
|
self.connect_to_unowned(port)
|
||||||
|
} else {
|
||||||
|
Ok(Missing)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** Connect to a matching port by reference. */
|
||||||
|
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
||||||
|
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||||
|
Connected
|
||||||
|
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||||
|
Connected
|
||||||
|
} else {
|
||||||
|
Mismatch
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/** Connect to an owned matching port by reference. */
|
||||||
|
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
||||||
|
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||||
|
Connected
|
||||||
|
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||||
|
Connected
|
||||||
|
} else {
|
||||||
|
Mismatch
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
11
device/device_type.rs
Normal file
11
device/device_type.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub type MidiData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
|
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||||
|
|
||||||
|
pub type CollectedMidiInput<'a> = Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
|
||||||
|
|
||||||
|
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||||
|
|
||||||
|
pub type MidiSample = (Option<u7>, Arc<RwLock<crate::Sample>>);
|
||||||
596
device/editor.rs
596
device/editor.rs
|
|
@ -1,596 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Contains state for viewing and editing a clip.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut editor = tek_device::MidiEditor {
|
|
||||||
/// mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
|
||||||
/// size: Default::default(),
|
|
||||||
/// //keys: Default::default(),
|
|
||||||
/// };
|
|
||||||
/// let _ = editor.put_note(true);
|
|
||||||
/// let _ = editor.put_note(false);
|
|
||||||
/// let _ = editor.clip_status();
|
|
||||||
/// let _ = editor.edit_status();
|
|
||||||
/// ```
|
|
||||||
pub struct MidiEditor {
|
|
||||||
/// Size of editor on screen
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// View mode and state of editor
|
|
||||||
pub mode: PianoHorizontal,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A clip, rendered as a horizontal piano roll.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let piano: tek_device::PianoHorizontal = Default::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Default)] pub struct PianoHorizontal {
|
|
||||||
pub clip: Option<Arc<RwLock<MidiClip>>>,
|
|
||||||
/// Buffer where the whole clip is rerendered on change
|
|
||||||
pub buffer: Arc<RwLock<BigBuffer>>,
|
|
||||||
/// Size of actual notes area
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// The display window
|
|
||||||
pub range: MidiSelection,
|
|
||||||
/// The note cursor
|
|
||||||
pub point: MidiCursor,
|
|
||||||
/// The highlight color palette
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Width of the keyboard
|
|
||||||
pub keys_width: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OctaveVertical { on: [bool; 12], colors: [Color; 3] }
|
|
||||||
|
|
||||||
/// ```
|
|
||||||
/// struct TestEditorHost(Option<MidiEditor>);
|
|
||||||
/// has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
|
||||||
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
|
|
||||||
/// let _ = host.editor();
|
|
||||||
/// let _ = host.editor_mut();
|
|
||||||
/// let _ = host.is_editing();
|
|
||||||
/// let _ = host.editor_w();
|
|
||||||
/// let _ = host.editor_h();
|
|
||||||
/// ```
|
|
||||||
pub trait HasEditor: Has<Option<MidiEditor>> {
|
|
||||||
fn editor (&self) -> Option<&MidiEditor> {
|
|
||||||
self.get().as_ref()
|
|
||||||
}
|
|
||||||
fn editor_mut (&mut self) -> Option<&mut MidiEditor> {
|
|
||||||
self.get_mut().as_mut()
|
|
||||||
}
|
|
||||||
fn is_editing (&self) -> bool {
|
|
||||||
self.editor().is_some()
|
|
||||||
}
|
|
||||||
fn editor_w (&self) -> usize {
|
|
||||||
self.editor().map(|e|e.size.w()).unwrap_or(0) as usize
|
|
||||||
}
|
|
||||||
fn editor_h (&self) -> usize {
|
|
||||||
self.editor().map(|e|e.size.h()).unwrap_or(0) as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Has<Option<MidiEditor>>> HasEditor for T {}
|
|
||||||
|
|
||||||
has!(Measure<TuiOut>: |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<RwLock<MidiClip>>| {
|
|
||||||
let model = Self::from(Some(clip.clone()));
|
|
||||||
model.redraw();
|
|
||||||
model
|
|
||||||
});
|
|
||||||
|
|
||||||
from!(MidiEditor: |clip: Option<Arc<RwLock<MidiClip>>>| {
|
|
||||||
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<Arc<RwLock<MidiClip>>> { 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<TuiOut> + '_ {
|
|
||||||
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<TuiOut> + '_ {
|
|
||||||
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<Arc<RwLock<MidiClip>>> { self.mode.clip() }
|
|
||||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
|
|
||||||
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
|
||||||
}
|
|
||||||
|
|
||||||
def_command!(MidiEditCommand: |editor: MidiEditor| {
|
|
||||||
Show { clip: Option<Arc<RwLock<MidiClip>>> } => {
|
|
||||||
editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) },
|
|
||||||
DeleteNote => {
|
|
||||||
editor.redraw(); todo!() },
|
|
||||||
AppendNote { advance: bool } => {
|
|
||||||
editor.put_note(*advance); editor.redraw(); Ok(None) },
|
|
||||||
SetNotePos { pos: usize } => {
|
|
||||||
editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) },
|
|
||||||
SetNoteLen { len: usize } => {
|
|
||||||
editor.set_note_len(*len); editor.redraw(); Ok(None) },
|
|
||||||
SetNoteScroll { scroll: usize } => {
|
|
||||||
editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) },
|
|
||||||
SetTimePos { pos: usize } => {
|
|
||||||
editor.set_time_pos(*pos); editor.redraw(); Ok(None) },
|
|
||||||
SetTimeScroll { scroll: usize } => {
|
|
||||||
editor.set_time_start(*scroll); editor.redraw(); Ok(None) },
|
|
||||||
SetTimeZoom { zoom: usize } => {
|
|
||||||
editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) },
|
|
||||||
SetTimeLock { lock: bool } => {
|
|
||||||
editor.set_time_lock(*lock); editor.redraw(); Ok(None) },
|
|
||||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
|
||||||
});
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for MidiEditor {
|
|
||||||
fn draw (&self, to: &mut TuiOut) { self.content().draw(to) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layout<TuiOut> for MidiEditor {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> { self.content().layout(to) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasContent<TuiOut> for MidiEditor {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
has!(Measure<TuiOut>:|self:PianoHorizontal|self.size);
|
|
||||||
|
|
||||||
impl PianoHorizontal {
|
|
||||||
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
|
|
||||||
-> impl Iterator<Item=(usize, u16, usize)>
|
|
||||||
{
|
|
||||||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<TuiOut> for PianoHorizontal {
|
|
||||||
fn draw (&self, to: &mut TuiOut) { self.content().draw(to) }
|
|
||||||
}
|
|
||||||
impl Layout<TuiOut> for PianoHorizontal {
|
|
||||||
fn layout (&self, to: XYWH<u16>) -> XYWH<u16> { self.content().layout(to) }
|
|
||||||
}
|
|
||||||
impl HasContent<TuiOut> for PianoHorizontal {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
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<TuiOut> {
|
|
||||||
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<TuiOut> {
|
|
||||||
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<TuiOut> {
|
|
||||||
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<TuiOut> + '_ {
|
|
||||||
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<Arc<RwLock<MidiClip>>> { &self.clip }
|
|
||||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { &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<RwLock<MidiClip>>>) {
|
|
||||||
*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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_key (note: usize) -> &'static str {
|
|
||||||
match note % 12 {
|
|
||||||
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
|
|
||||||
10 | 8 | 6 | 3 | 1 => " ",
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<TuiOut> for OctaveVertical {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
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), "▟"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update sequencer playhead indicator
|
|
||||||
//self.now().set(0.);
|
|
||||||
//if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip {
|
|
||||||
//let clip = clip.read().unwrap();
|
|
||||||
//if *playing.read().unwrap() == *clip {
|
|
||||||
//let pulse = self.current().pulse.get();
|
|
||||||
//let start = started_at.pulse.get();
|
|
||||||
//let now = (pulse - start) % clip.length as f64;
|
|
||||||
//self.now().set(now);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
310
device/lv2.rs
310
device/lv2.rs
|
|
@ -1,310 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// A LV2 plugin.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Lv2 {
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
pub name: Arc<str>,
|
|
||||||
pub path: Option<Arc<str>>,
|
|
||||||
pub selected: usize,
|
|
||||||
pub mapping: bool,
|
|
||||||
pub midi_ins: Vec<Port<MidiIn>>,
|
|
||||||
pub midi_outs: Vec<Port<MidiOut>>,
|
|
||||||
pub audio_ins: Vec<Port<AudioIn>>,
|
|
||||||
pub audio_outs: Vec<Port<AudioOut>>,
|
|
||||||
|
|
||||||
pub lv2_world: livi::World,
|
|
||||||
pub lv2_instance: livi::Instance,
|
|
||||||
pub lv2_plugin: livi::Plugin,
|
|
||||||
pub lv2_features: Arc<livi::Features>,
|
|
||||||
pub lv2_port_list: Vec<livi::Port>,
|
|
||||||
pub lv2_input_buffer: Vec<livi::event::LV2AtomSequence>,
|
|
||||||
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Lv2 {
|
|
||||||
|
|
||||||
pub fn new (
|
|
||||||
jack: &Jack<'static>,
|
|
||||||
name: &str,
|
|
||||||
uri: &str,
|
|
||||||
) -> Usually<Self> {
|
|
||||||
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::<Vec<_>>(),
|
|
||||||
lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
|
|
||||||
lv2_ui_thread: None,
|
|
||||||
lv2_world,
|
|
||||||
lv2_features,
|
|
||||||
lv2_plugin,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const INPUT_BUFFER: usize = 1024;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
|
||||||
//let counts = plugin.port_counts();
|
|
||||||
//let mut jack = Jack::new(name)?;
|
|
||||||
//for i in 0..counts.atom_sequence_inputs {
|
|
||||||
//jack = jack.midi_in(&format!("midi-in-{i}"))
|
|
||||||
//}
|
|
||||||
//for i in 0..counts.atom_sequence_outputs {
|
|
||||||
//jack = jack.midi_out(&format!("midi-out-{i}"));
|
|
||||||
//}
|
|
||||||
//for i in 0..counts.audio_inputs {
|
|
||||||
//jack = jack.audio_in(&format!("audio-in-{i}"));
|
|
||||||
//}
|
|
||||||
//for i in 0..counts.audio_outputs {
|
|
||||||
//jack = jack.audio_out(&format!("audio-out-{i}"));
|
|
||||||
//}
|
|
||||||
//Ok(jack)
|
|
||||||
//}
|
|
||||||
|
|
||||||
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<TuiOut> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) {
|
|
||||||
let style = Style::default().gray();
|
|
||||||
let label1 = format!(" {}", state.name);
|
|
||||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
|
||||||
if let Some(ref path) = state.path {
|
|
||||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
|
||||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
|
||||||
}
|
|
||||||
//Ok(Rect { x, y, width: w, height: 1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
//handle!(TuiIn: |self:Plugin, from|{
|
|
||||||
//match from.event() {
|
|
||||||
//kpat!(KeyCode::Up) => {
|
|
||||||
//self.selected = self.selected.saturating_sub(1);
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//kpat!(KeyCode::Down) => {
|
|
||||||
//self.selected = (self.selected + 1).min(match &self.plugin {
|
|
||||||
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
|
||||||
//_ => unimplemented!()
|
|
||||||
//});
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//kpat!(KeyCode::PageUp) => {
|
|
||||||
//self.selected = self.selected.saturating_sub(8);
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//kpat!(KeyCode::PageDown) => {
|
|
||||||
//self.selected = (self.selected + 10).min(match &self.plugin {
|
|
||||||
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
|
||||||
//_ => unimplemented!()
|
|
||||||
//});
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//kpat!(KeyCode::Char(',')) => {
|
|
||||||
//match self.plugin.as_mut() {
|
|
||||||
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
|
||||||
//let index = port_list[self.selected].index;
|
|
||||||
//if let Some(value) = instance.control_input(index) {
|
|
||||||
//instance.set_control_input(index, value - 0.01);
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => {}
|
|
||||||
//}
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//kpat!(KeyCode::Char('.')) => {
|
|
||||||
//match self.plugin.as_mut() {
|
|
||||||
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
|
||||||
//let index = port_list[self.selected].index;
|
|
||||||
//if let Some(value) = instance.control_input(index) {
|
|
||||||
//instance.set_control_input(index, value + 0.01);
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => {}
|
|
||||||
//}
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//kpat!(KeyCode::Char('g')) => {
|
|
||||||
//match self.plugin {
|
|
||||||
////Some(PluginKind::LV2(ref mut plugin)) => {
|
|
||||||
////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
|
|
||||||
////},
|
|
||||||
//Some(_) => unreachable!(),
|
|
||||||
//None => {}
|
|
||||||
//}
|
|
||||||
//Ok(Some(true))
|
|
||||||
//},
|
|
||||||
//_ => Ok(None)
|
|
||||||
//}
|
|
||||||
//});
|
|
||||||
|
|
||||||
//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
|
|
||||||
//let mut name = String::new();
|
|
||||||
//let mut path = String::new();
|
|
||||||
//atom!(atom in args {
|
|
||||||
//Atom::Map(map) => {
|
|
||||||
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
|
|
||||||
//name = String::from(*n);
|
|
||||||
//}
|
|
||||||
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
|
|
||||||
//path = String::from(*p);
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => panic!("unexpected in lv2 '{name}'"),
|
|
||||||
//});
|
|
||||||
//Plugin::new_lv2(jack, &name, &path)
|
|
||||||
//});
|
|
||||||
|
|
||||||
//pub struct LV2PluginUI {
|
|
||||||
//write: (),
|
|
||||||
//controller: (),
|
|
||||||
//widget: (),
|
|
||||||
//features: (),
|
|
||||||
//transfer: (),
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(feature = "lv2_gui")]
|
|
||||||
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
|
||||||
Ok(spawn(move||{
|
|
||||||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
|
||||||
event_loop.set_control_flow(ControlFlow::Wait);
|
|
||||||
event_loop.run_app(&mut ui).unwrap()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "lv2_gui")]
|
|
||||||
/// A LV2 plugin's X11 UI.
|
|
||||||
pub struct LV2PluginUI {
|
|
||||||
pub window: Option<Window>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "lv2_gui")]
|
|
||||||
impl LV2PluginUI {
|
|
||||||
pub fn new () -> Usually<Self> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "lv2_gui")]
|
|
||||||
fn lv2_ui_instantiate (kind: &str) {
|
|
||||||
//let host = Suil
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub enum MeteringMode {
|
|
||||||
#[default]
|
|
||||||
Rms,
|
|
||||||
Log10,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct Log10Meter(pub f32);
|
|
||||||
impl Layout<TuiOut> for Log10Meter {}
|
|
||||||
impl Draw<TuiOut> 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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_log10 (samples: &[f32]) -> f32 {
|
|
||||||
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
|
||||||
let count = samples.len() as f32;
|
|
||||||
10. * (total / count).log10()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct RmsMeter(pub f32);
|
|
||||||
impl Layout<TuiOut> for RmsMeter {}
|
|
||||||
impl Draw<TuiOut> 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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_rms (samples: &[f32]) -> f32 {
|
|
||||||
let sum = samples.iter()
|
|
||||||
.map(|s|*s)
|
|
||||||
.reduce(|sum, sample|sum + sample.abs())
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
(sum / samples.len() as f32).sqrt()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
|
|
||||||
col!(
|
|
||||||
FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
|
|
||||||
Fixed::XY(if value >= 0.0 { 13 }
|
|
||||||
else if value >= -1.0 { 12 }
|
|
||||||
else if value >= -2.0 { 11 }
|
|
||||||
else if value >= -3.0 { 10 }
|
|
||||||
else if value >= -4.0 { 9 }
|
|
||||||
else if value >= -6.0 { 8 }
|
|
||||||
else if value >= -9.0 { 7 }
|
|
||||||
else if value >= -12.0 { 6 }
|
|
||||||
else if value >= -15.0 { 5 }
|
|
||||||
else if value >= -20.0 { 4 }
|
|
||||||
else if value >= -25.0 { 3 }
|
|
||||||
else if value >= -30.0 { 2 }
|
|
||||||
else if value >= -40.0 { 1 }
|
|
||||||
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
|
|
||||||
else if value >= -3.0 { Yellow }
|
|
||||||
else { Green }, ())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let left = format!("L/{:>+9.3}", values[0]);
|
|
||||||
let right = format!("R/{:>+9.3}", values[1]);
|
|
||||||
Bsp::s(left, right)
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
#[allow(unused)] use crate::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub enum MixingMode {
|
|
||||||
#[default]
|
|
||||||
Summing,
|
|
||||||
Average,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mix_summing <const N: usize> (
|
|
||||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
|
||||||
) -> bool {
|
|
||||||
let channels = buffer.len();
|
|
||||||
for index in 0..frames {
|
|
||||||
if let Some(frame) = next() {
|
|
||||||
for (channel, sample) in frame.iter().enumerate() {
|
|
||||||
let channel = channel % channels;
|
|
||||||
buffer[channel][index] += sample * gain;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mix_average <const N: usize> (
|
|
||||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
|
||||||
) -> bool {
|
|
||||||
let channels = buffer.len();
|
|
||||||
for index in 0..frames {
|
|
||||||
if let Some(frame) = next() {
|
|
||||||
for (channel, sample) in frame.iter().enumerate() {
|
|
||||||
let channel = channel % channels;
|
|
||||||
let value = buffer[channel][index];
|
|
||||||
buffer[channel][index] = (value + sample * gain) / 2.0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
414
device/pool.rs
414
device/pool.rs
|
|
@ -1,414 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Pool {
|
|
||||||
pub visible: bool,
|
|
||||||
/// Selected clip
|
|
||||||
pub clip: AtomicUsize,
|
|
||||||
/// Mode switch
|
|
||||||
pub mode: Option<PoolMode>,
|
|
||||||
/// Collection of clips
|
|
||||||
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
|
||||||
/// Embedded file browse
|
|
||||||
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
|
||||||
}
|
|
||||||
impl Default for Pool {
|
|
||||||
fn default () -> Self {
|
|
||||||
//use PoolMode::*;
|
|
||||||
Self {
|
|
||||||
visible: true,
|
|
||||||
clip: 0.into(),
|
|
||||||
mode: None,
|
|
||||||
clips: Arc::from(RwLock::from(vec![])),
|
|
||||||
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<PoolMode> {
|
|
||||||
&self.mode
|
|
||||||
}
|
|
||||||
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
|
||||||
&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<RwLock<MidiClip>>) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Modes for clip pool
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PoolMode {
|
|
||||||
/// Renaming a pattern
|
|
||||||
Rename(usize, Arc<str>),
|
|
||||||
/// Editing the length of a pattern
|
|
||||||
Length(usize, usize, ClipLengthFocus),
|
|
||||||
/// Load clip from disk
|
|
||||||
Import(usize, Browse),
|
|
||||||
/// Save clip to disk
|
|
||||||
Export(usize, Browse),
|
|
||||||
}
|
|
||||||
/// Focused field of `ClipLength`
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum ClipLengthFocus {
|
|
||||||
/// Editing the number of bars
|
|
||||||
Bar,
|
|
||||||
/// Editing the number of beats
|
|
||||||
Beat,
|
|
||||||
/// Editing the number of ticks
|
|
||||||
Tick,
|
|
||||||
}
|
|
||||||
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, }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Displays and edits clip length.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ClipLength {
|
|
||||||
/// Pulses per beat (quaver)
|
|
||||||
ppq: usize,
|
|
||||||
/// Beats per bar
|
|
||||||
bpb: usize,
|
|
||||||
/// Length of clip in pulses
|
|
||||||
pulses: usize,
|
|
||||||
/// Selected subdivision
|
|
||||||
pub focus: Option<ClipLengthFocus>,
|
|
||||||
}
|
|
||||||
impl ClipLength {
|
|
||||||
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> 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<str> {
|
|
||||||
format!("{}", self.bars()).into()
|
|
||||||
}
|
|
||||||
pub fn beats_string (&self) -> Arc<str> {
|
|
||||||
format!("{}", self.beats()).into()
|
|
||||||
}
|
|
||||||
pub fn ticks_string (&self) -> Arc<str> {
|
|
||||||
format!("{:>02}", self.ticks()).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
|
||||||
pub trait HasClips {
|
|
||||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
|
||||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
|
||||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
|
||||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
|
||||||
self.clips_mut().push(clip.clone());
|
|
||||||
(self.clips().len() - 1, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[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<RwLock<MidiClip>>|{
|
|
||||||
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<str> { 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() }
|
|
||||||
}
|
|
||||||
def_command!(PoolCommand: |pool: Pool| {
|
|
||||||
// Toggle visibility of pool
|
|
||||||
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
|
|
||||||
// Select a clip from the clip pool
|
|
||||||
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
|
|
||||||
// Update the contents of the clip pool
|
|
||||||
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
|
|
||||||
// Rename a clip
|
|
||||||
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
|
|
||||||
// Change the length of a clip
|
|
||||||
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
|
|
||||||
// Import from file
|
|
||||||
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
|
||||||
command.delegate(browse, |command|Self::Import{command})?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}),
|
|
||||||
// Export to file
|
|
||||||
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
|
||||||
command.delegate(browse, |command|Self::Export{command})?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
def_command!(PoolClipCommand: |pool: Pool| {
|
|
||||||
Delete { index: usize } => {
|
|
||||||
let index = *index;
|
|
||||||
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
|
||||||
Ok(Some(Self::Add { index, clip }))
|
|
||||||
},
|
|
||||||
Swap { index: usize, other: usize } => {
|
|
||||||
let index = *index;
|
|
||||||
let other = *other;
|
|
||||||
pool.clips_mut().swap(index, other);
|
|
||||||
Ok(Some(Self::Swap { index, other }))
|
|
||||||
},
|
|
||||||
Export { index: usize, path: PathBuf } => {
|
|
||||||
todo!("export clip to midi file");
|
|
||||||
},
|
|
||||||
Add { index: usize, clip: MidiClip } => {
|
|
||||||
let index = *index;
|
|
||||||
let mut index = index;
|
|
||||||
let clip = Arc::new(RwLock::new(clip.clone()));
|
|
||||||
let mut clips = pool.clips_mut();
|
|
||||||
if index >= clips.len() {
|
|
||||||
index = clips.len();
|
|
||||||
clips.push(clip)
|
|
||||||
} else {
|
|
||||||
clips.insert(index, clip);
|
|
||||||
}
|
|
||||||
Ok(Some(Self::Delete { index }))
|
|
||||||
},
|
|
||||||
Import { index: usize, path: PathBuf } => {
|
|
||||||
let index = *index;
|
|
||||||
let bytes = std::fs::read(&path)?;
|
|
||||||
let smf = Smf::parse(bytes.as_slice())?;
|
|
||||||
let mut t = 0u32;
|
|
||||||
let mut events = vec![];
|
|
||||||
for track in smf.tracks.iter() {
|
|
||||||
for event in track.iter() {
|
|
||||||
t += event.delta.as_int();
|
|
||||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
|
||||||
events.push((t, channel.as_int(), message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
|
||||||
for event in events.iter() {
|
|
||||||
clip.notes[event.0 as usize].push(event.2);
|
|
||||||
}
|
|
||||||
Ok(Self::Add { index, clip }.execute(pool)?)
|
|
||||||
},
|
|
||||||
SetName { index: usize, name: Arc<str> } => {
|
|
||||||
let index = *index;
|
|
||||||
let clip = &mut pool.clips_mut()[index];
|
|
||||||
let old_name = clip.read().unwrap().name.clone();
|
|
||||||
clip.write().unwrap().name = name.clone();
|
|
||||||
Ok(Some(Self::SetName { index, name: old_name }))
|
|
||||||
},
|
|
||||||
SetLength { index: usize, length: usize } => {
|
|
||||||
let index = *index;
|
|
||||||
let clip = &mut pool.clips_mut()[index];
|
|
||||||
let old_len = clip.read().unwrap().length;
|
|
||||||
clip.write().unwrap().length = *length;
|
|
||||||
Ok(Some(Self::SetLength { index, length: old_len }))
|
|
||||||
},
|
|
||||||
SetColor { index: usize, color: ItemColor } => {
|
|
||||||
let index = *index;
|
|
||||||
let mut color = ItemTheme::from(*color);
|
|
||||||
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
|
||||||
Ok(Some(Self::SetColor { index, color: color.base }))
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
def_command!(RenameCommand: |pool: Pool| {
|
|
||||||
Begin => unreachable!(),
|
|
||||||
Cancel => {
|
|
||||||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
|
||||||
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
Confirm => {
|
|
||||||
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
|
|
||||||
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
Set { value: Arc<str> } => {
|
|
||||||
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
|
|
||||||
pool.clips()[clip].write().unwrap().name = value.clone();
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
});
|
|
||||||
def_command!(CropCommand: |pool: Pool| {
|
|
||||||
Begin => unreachable!(),
|
|
||||||
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
|
|
||||||
Set { length: usize } => {
|
|
||||||
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
|
||||||
= pool.mode_mut().clone()
|
|
||||||
{
|
|
||||||
let old_length;
|
|
||||||
{
|
|
||||||
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
|
||||||
old_length = Some(clip.read().unwrap().length);
|
|
||||||
clip.write().unwrap().length = *length;
|
|
||||||
}
|
|
||||||
*pool.mode_mut() = None;
|
|
||||||
return Ok(old_length.map(|length|Self::Set { length }))
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
Next => {
|
|
||||||
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
|
|
||||||
},
|
|
||||||
Prev => {
|
|
||||||
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
|
|
||||||
},
|
|
||||||
Inc => {
|
|
||||||
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
|
||||||
match focus {
|
|
||||||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
|
||||||
ClipLengthFocus::Beat => { *length += PPQ },
|
|
||||||
ClipLengthFocus::Tick => { *length += 1 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
Dec => {
|
|
||||||
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
|
||||||
match focus {
|
|
||||||
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
|
||||||
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
|
||||||
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pub struct PoolView<'a>(pub &'a Pool);
|
|
||||||
impl<'a> HasContent<TuiOut> for PoolView<'a> {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> {
|
|
||||||
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<RwLock<MidiClip>>, 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<TuiOut> for ClipLength {
|
|
||||||
fn content (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref()
|
|
||||||
//.map(|p|Take::take(p, iter))
|
|
||||||
//.transpose()?
|
|
||||||
//.flatten()));
|
|
||||||
|
|
||||||
577
device/port.rs
577
device/port.rs
|
|
@ -1,577 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
def_sizes_iter!(InputsSizes => MidiInput);
|
|
||||||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
|
||||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
|
||||||
|
|
||||||
pub(crate) use ConnectName::*;
|
|
||||||
pub(crate) use ConnectScope::*;
|
|
||||||
pub(crate) use ConnectStatus::*;
|
|
||||||
|
|
||||||
#[derive(Debug)] pub struct AudioInput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
port: Port<AudioIn>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)] pub struct AudioOutput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
port: Port<AudioOut>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)] pub struct MidiInput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
port: Port<MidiIn>,
|
|
||||||
/// List of currently held notes.
|
|
||||||
held: Arc<RwLock<[bool;128]>>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)] pub struct MidiOutput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
port: Port<MidiOut>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
/// List of currently held notes.
|
|
||||||
held: Arc<RwLock<[bool;128]>>,
|
|
||||||
/// Buffer
|
|
||||||
note_buffer: Vec<u8>,
|
|
||||||
/// Buffer
|
|
||||||
output_buffer: Vec<Vec<Vec<u8>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RegisterPorts: HasJack<'static> {
|
|
||||||
/// Register a MIDI input port.
|
|
||||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
|
||||||
/// Register a MIDI output port.
|
|
||||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
|
||||||
/// Register an audio input port.
|
|
||||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
|
||||||
/// Register an audio output port.
|
|
||||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<J: HasJack<'static>> RegisterPorts for J {
|
|
||||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
|
||||||
MidiInput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
|
||||||
MidiOutput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
|
||||||
AudioInput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
|
||||||
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()));
|
|
||||||
|
|
||||||
pub trait JackPort: HasJack<'static> {
|
|
||||||
type Port: PortSpec + Default;
|
|
||||||
type Pair: PortSpec + Default;
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
||||||
-> Usually<Self> where Self: Sized;
|
|
||||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
|
||||||
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
|
||||||
.map_err(|e|e.into())
|
|
||||||
}
|
|
||||||
fn port_name (&self) -> &Arc<str>;
|
|
||||||
fn connections (&self) -> &[Connect];
|
|
||||||
fn port (&self) -> &Port<Self::Port>;
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
|
||||||
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
|
||||||
fn close (self) -> Usually<()> where Self: Sized {
|
|
||||||
let jack = self.jack().clone();
|
|
||||||
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
|
||||||
}
|
|
||||||
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
|
||||||
self.with_client(|c|c.ports(re_name, re_type, flags))
|
|
||||||
}
|
|
||||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
|
||||||
self.with_client(|c|c.port_by_id(id))
|
|
||||||
}
|
|
||||||
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
|
||||||
self.with_client(|c|c.port_by_name(name.as_ref()))
|
|
||||||
}
|
|
||||||
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
|
||||||
for connect in self.connections().iter() {
|
|
||||||
//panic!("{connect:?}");
|
|
||||||
let status = match &connect.name {
|
|
||||||
Exact(name) => self.connect_exact(name),
|
|
||||||
RegExp(re) => self.connect_regexp(re, connect.scope),
|
|
||||||
}?;
|
|
||||||
*connect.status.write().unwrap() = status;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn connect_exact <'k> (&'k self, name: &str) ->
|
|
||||||
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
|
||||||
{
|
|
||||||
self.with_client(move|c|{
|
|
||||||
let mut status = vec![];
|
|
||||||
for port in c.ports(None, None, PortFlags::empty()).iter() {
|
|
||||||
if port.as_str() == &*name {
|
|
||||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
|
||||||
let port_status = self.connect_to_unowned(&port)?;
|
|
||||||
let name = port.name()?.into();
|
|
||||||
status.push((port, name, port_status));
|
|
||||||
if port_status == Connected {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(status)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn connect_regexp <'k> (
|
|
||||||
&'k self, re: &str, scope: ConnectScope
|
|
||||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
|
||||||
self.with_client(move|c|{
|
|
||||||
let mut status = vec![];
|
|
||||||
let ports = c.ports(Some(&re), None, PortFlags::empty());
|
|
||||||
for port in ports.iter() {
|
|
||||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
|
||||||
let port_status = self.connect_to_unowned(&port)?;
|
|
||||||
let name = port.name()?.into();
|
|
||||||
status.push((port, name, port_status));
|
|
||||||
if port_status == Connected && scope == One {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(status)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/** Connect to a matching port by name. */
|
|
||||||
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
|
||||||
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
|
||||||
self.connect_to_unowned(port)
|
|
||||||
} else {
|
|
||||||
Ok(Missing)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/** Connect to a matching port by reference. */
|
|
||||||
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
|
||||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
|
||||||
Connected
|
|
||||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
|
||||||
Connected
|
|
||||||
} else {
|
|
||||||
Mismatch
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/** Connect to an owned matching port by reference. */
|
|
||||||
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
|
||||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
|
||||||
Connected
|
|
||||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
|
||||||
Connected
|
|
||||||
} else {
|
|
||||||
Mismatch
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum ConnectName {
|
|
||||||
/** Exact match */
|
|
||||||
Exact(Arc<str>),
|
|
||||||
/** Match regular expression */
|
|
||||||
RegExp(Arc<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
|
||||||
One,
|
|
||||||
All
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
|
||||||
Missing,
|
|
||||||
Disconnected,
|
|
||||||
Connected,
|
|
||||||
Mismatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)] pub struct Connect {
|
|
||||||
pub name: ConnectName,
|
|
||||||
pub scope: ConnectScope,
|
|
||||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
|
||||||
pub info: Arc<str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connect {
|
|
||||||
pub fn collect (exact: &[impl AsRef<str>], re: &[impl AsRef<str>], re_all: &[impl AsRef<str>])
|
|
||||||
-> Vec<Self>
|
|
||||||
{
|
|
||||||
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<str>) -> Self {
|
|
||||||
let info = format!("=:{}", name.as_ref()).into();
|
|
||||||
let name = Exact(name.as_ref().into());
|
|
||||||
Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info }
|
|
||||||
}
|
|
||||||
pub fn regexp (name: impl AsRef<str>) -> Self {
|
|
||||||
let info = format!("~:{}", name.as_ref()).into();
|
|
||||||
let name = RegExp(name.as_ref().into());
|
|
||||||
Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info }
|
|
||||||
}
|
|
||||||
pub fn regexp_all (name: impl AsRef<str>) -> Self {
|
|
||||||
let info = format!("+:{}", name.as_ref()).into();
|
|
||||||
let name = RegExp(name.as_ref().into());
|
|
||||||
Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info }
|
|
||||||
}
|
|
||||||
pub fn info (&self) -> Arc<str> {
|
|
||||||
let status = {
|
|
||||||
let status = self.status.read().unwrap();
|
|
||||||
let mut ok = 0;
|
|
||||||
for (_, _, state) in status.iter() {
|
|
||||||
if *state == Connected {
|
|
||||||
ok += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format!("{ok}/{}", status.len())
|
|
||||||
};
|
|
||||||
let scope = match self.scope {
|
|
||||||
One => " ", All => "*",
|
|
||||||
};
|
|
||||||
let name = match &self.name {
|
|
||||||
Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"),
|
|
||||||
};
|
|
||||||
format!(" ({}) {} {}", status, scope, name).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def_command!(MidiInputCommand: |port: MidiInput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { midi_out: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { midi_in: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
def_command!(AudioInputCommand: |port: AudioInput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { audio_out: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { audio_in: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
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<str> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
fn port (&self) -> &Port<Self::Port> {
|
|
||||||
&self.port
|
|
||||||
}
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
|
||||||
&mut self.port
|
|
||||||
}
|
|
||||||
fn into_port (self) -> Port<Self::Port> {
|
|
||||||
self.port
|
|
||||||
}
|
|
||||||
fn connections (&self) -> &[Connect] {
|
|
||||||
self.connections.as_slice()
|
|
||||||
}
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
||||||
-> Usually<Self> 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<Item=(usize, LiveEvent<'a>, &'a [u8])> {
|
|
||||||
parse_midi_input(self.port().iter(scope))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Has<Vec<MidiInput>>> HasMidiIns for T {
|
|
||||||
fn midi_ins (&self) -> &Vec<MidiInput> {
|
|
||||||
self.get()
|
|
||||||
}
|
|
||||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> {
|
|
||||||
self.get_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for thing that may receive MIDI.
|
|
||||||
pub trait HasMidiIns {
|
|
||||||
fn midi_ins (&self) -> &Vec<MidiInput>;
|
|
||||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
|
||||||
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
|
||||||
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
|
||||||
self.midi_ins().iter()
|
|
||||||
.map(|port|port.port().iter(scope)
|
|
||||||
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
fn midi_ins_with_sizes <'a> (&'a self) ->
|
|
||||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
|
||||||
{
|
|
||||||
let mut y = 0;
|
|
||||||
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
|
||||||
let height = 1 + input.connections().len();
|
|
||||||
let data = (i, input.port_name(), input.connections(), y, y + height);
|
|
||||||
y += height;
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type CollectedMidiInput<'a> = Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
|
|
||||||
|
|
||||||
impl<T: HasMidiIns + HasJack<'static>> 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// May create new MIDI input ports.
|
|
||||||
pub trait AddMidiIn {
|
|
||||||
fn midi_in_add (&mut self) -> Usually<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<str> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
fn port (&self) -> &Port<Self::Port> {
|
|
||||||
&self.port
|
|
||||||
}
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
|
||||||
&mut self.port
|
|
||||||
}
|
|
||||||
fn into_port (self) -> Port<Self::Port> {
|
|
||||||
self.port
|
|
||||||
}
|
|
||||||
fn connections (&self) -> &[Connect] {
|
|
||||||
self.connections.as_slice()
|
|
||||||
}
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
||||||
-> Usually<Self> 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<T: Has<Vec<MidiOutput>>> HasMidiOuts for T {
|
|
||||||
fn midi_outs (&self) -> &Vec<MidiOutput> {
|
|
||||||
self.get()
|
|
||||||
}
|
|
||||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> {
|
|
||||||
self.get_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Trait for thing that may output MIDI.
|
|
||||||
pub trait HasMidiOuts {
|
|
||||||
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
|
||||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
|
||||||
fn midi_outs_with_sizes <'a> (&'a self) ->
|
|
||||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
|
||||||
{
|
|
||||||
let mut y = 0;
|
|
||||||
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
|
||||||
let height = 1 + output.connections().len();
|
|
||||||
let data = (i, output.port_name(), output.connections(), y, y + height);
|
|
||||||
y += height;
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
|
||||||
for port in self.midi_outs_mut().iter_mut() {
|
|
||||||
port.buffer_emit(scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trail for thing that may gain new MIDI ports.
|
|
||||||
impl<T: HasMidiOuts + HasJack<'static>> 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// May create new MIDI output ports.
|
|
||||||
pub trait AddMidiOut {
|
|
||||||
fn midi_out_add (&mut self) -> Usually<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<str> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
fn port (&self) -> &Port<Self::Port> {
|
|
||||||
&self.port
|
|
||||||
}
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
|
||||||
&mut self.port
|
|
||||||
}
|
|
||||||
fn into_port (self) -> Port<Self::Port> {
|
|
||||||
self.port
|
|
||||||
}
|
|
||||||
fn connections (&self) -> &[Connect] {
|
|
||||||
self.connections.as_slice()
|
|
||||||
}
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
||||||
-> Usually<Self> 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<str> {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
fn port (&self) -> &Port<Self::Port> {
|
|
||||||
&self.port
|
|
||||||
}
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
|
||||||
&mut self.port
|
|
||||||
}
|
|
||||||
fn into_port (self) -> Port<Self::Port> {
|
|
||||||
self.port
|
|
||||||
}
|
|
||||||
fn connections (&self) -> &[Connect] {
|
|
||||||
self.connections.as_slice()
|
|
||||||
}
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
||||||
-> Usually<Self> 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1116
device/sampler.rs
1116
device/sampler.rs
File diff suppressed because it is too large
Load diff
197
device/scene.rs
197
device/scene.rs
|
|
@ -1,197 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// A scene consists of a set of clips to play together.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let scene: tek_device::Scene = Default::default();
|
|
||||||
/// let _ = scene.pulses();
|
|
||||||
/// let _ = scene.is_playing(&[]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Scene {
|
|
||||||
/// Name of scene
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
def_sizes_iter!(ScenesSizes => Scene);
|
|
||||||
|
|
||||||
impl<T: Has<Vec<Scene>> + Send + Sync> HasScenes for T {}
|
|
||||||
|
|
||||||
pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
|
||||||
fn scenes (&self) -> &Vec<Scene> {
|
|
||||||
Has::<Vec<Scene>>::get(self)
|
|
||||||
}
|
|
||||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
|
||||||
Has::<Vec<Scene>>::get_mut(self)
|
|
||||||
}
|
|
||||||
/// Generate the default name for a new scene
|
|
||||||
fn scene_default_name (&self) -> Arc<str> {
|
|
||||||
format!("s{:3>}", self.scenes().len() + 1).into()
|
|
||||||
}
|
|
||||||
fn scene_longest_name (&self) -> usize {
|
|
||||||
self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasSceneScroll: HasScenes {
|
|
||||||
fn scene_scroll (&self) -> usize;
|
|
||||||
}
|
|
||||||
impl HasSceneScroll for Arrangement {
|
|
||||||
fn scene_scroll (&self) -> usize { self.scene_scroll }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
|
||||||
|
|
||||||
impl<T: HasScenes + HasTracks> AddScene for T {}
|
|
||||||
|
|
||||||
pub trait AddScene: HasScenes + HasTracks {
|
|
||||||
/// Add multiple scenes
|
|
||||||
fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
|
||||||
let scene_color_1 = ItemColor::random();
|
|
||||||
let scene_color_2 = ItemColor::random();
|
|
||||||
for i in 0..n {
|
|
||||||
let _ = self.scene_add(None, Some(
|
|
||||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Add a scene
|
|
||||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
|
||||||
-> Usually<(usize, &mut Scene)>
|
|
||||||
{
|
|
||||||
let scene = Scene {
|
|
||||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
|
||||||
clips: vec![None;self.tracks().len()],
|
|
||||||
color: color.unwrap_or_else(ItemTheme::random),
|
|
||||||
};
|
|
||||||
self.scenes_mut().push(scene);
|
|
||||||
let index = self.scenes().len() - 1;
|
|
||||||
Ok((index, &mut self.scenes_mut()[index]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scene {
|
|
||||||
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
|
||||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
|
||||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
|
||||||
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
|
||||||
}
|
|
||||||
|
|
||||||
def_command!(SceneCommand: |scene: Scene| {
|
|
||||||
SetSize { size: usize } => { todo!() },
|
|
||||||
SetZoom { size: usize } => { todo!() },
|
|
||||||
SetName { name: Arc<str> } =>
|
|
||||||
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
|
||||||
SetColor { color: ItemTheme } =>
|
|
||||||
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
|
||||||
});
|
|
||||||
|
|
||||||
impl<T: Has<Option<Scene>> + Send + Sync> HasScene for T {}
|
|
||||||
|
|
||||||
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
|
|
||||||
fn scene (&self) -> Option<&Scene> {
|
|
||||||
Has::<Option<Scene>>::get(self).as_ref()
|
|
||||||
}
|
|
||||||
fn scene_mut (&mut self) -> &mut Option<Scene> {
|
|
||||||
Has::<Option<Scene>>::get_mut(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ScenesView:
|
|
||||||
HasEditor +
|
|
||||||
HasSelection +
|
|
||||||
HasSceneScroll +
|
|
||||||
HasClipsSize +
|
|
||||||
Send +
|
|
||||||
Sync
|
|
||||||
{
|
|
||||||
/// Default scene height.
|
|
||||||
const H_SCENE: usize = 2;
|
|
||||||
/// Default editor height.
|
|
||||||
const H_EDITOR: usize = 15;
|
|
||||||
fn h_scenes (&self) -> u16;
|
|
||||||
fn w_side (&self) -> u16;
|
|
||||||
fn w_mid (&self) -> u16;
|
|
||||||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
|
||||||
let mut y = 0;
|
|
||||||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
|
||||||
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
|
||||||
8
|
|
||||||
} else {
|
|
||||||
Self::H_SCENE
|
|
||||||
};
|
|
||||||
if y + height <= self.clips_size().h() as usize {
|
|
||||||
let data = (s, scene, y, y + height);
|
|
||||||
y += height;
|
|
||||||
Some(data)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn view_scenes_names (&self) -> impl Content<TuiOut> {
|
|
||||||
Fixed::X(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() {
|
|
||||||
to.place(&self.view_scene_name(index, scene));
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Content<TuiOut> + 'a {
|
|
||||||
let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() {
|
|
||||||
7
|
|
||||||
} else {
|
|
||||||
Self::H_SCENE as u16
|
|
||||||
};
|
|
||||||
let bg = if self.selection().scene() == Some(index) {
|
|
||||||
scene.color.light.rgb
|
|
||||||
} else {
|
|
||||||
scene.color.base.rgb
|
|
||||||
};
|
|
||||||
let a = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "),
|
|
||||||
Tui::fg(Tui::g(255), Tui::bold(true, &scene.name)))));
|
|
||||||
let b = When::new(self.selection().scene() == Some(index) && self.is_editing(),
|
|
||||||
Fill::XY(Align::nw(Bsp::s(
|
|
||||||
self.editor().as_ref().map(|e|e.clip_status()),
|
|
||||||
self.editor().as_ref().map(|e|e.edit_status())))));
|
|
||||||
Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<RwLock<MidiClip>>> {
|
|
||||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//scene_scroll: Fill::Y(Fixed::X(1, ScrollbarV {
|
|
||||||
//offset: arrangement.scene_scroll,
|
|
||||||
//length: h_scenes_area as usize,
|
|
||||||
//total: h_scenes as usize,
|
|
||||||
//})),
|
|
||||||
//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref()
|
|
||||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
|
||||||
140
device/select.rs
140
device/select.rs
|
|
@ -1,140 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl<T: Has<Selection>> HasSelection for T {}
|
|
||||||
pub trait HasSelection: Has<Selection> {
|
|
||||||
fn selection (&self) -> &Selection { self.get() }
|
|
||||||
fn selection_mut (&mut self) -> &mut Selection { self.get_mut() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
|
||||||
pub enum Selection {
|
|
||||||
#[default]
|
|
||||||
/// Nothing is selected
|
|
||||||
Nothing,
|
|
||||||
/// The whole mix is selected
|
|
||||||
Mix,
|
|
||||||
/// A MIDI input is selected.
|
|
||||||
Input(usize),
|
|
||||||
/// A MIDI output is selected.
|
|
||||||
Output(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
#[cfg(feature = "scene")] Scene(usize),
|
|
||||||
/// A track is selected.
|
|
||||||
#[cfg(feature = "track")] Track(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
|
||||||
/// A track's MIDI input connection is selected.
|
|
||||||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
|
||||||
/// A track's MIDI output connection is selected.
|
|
||||||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
|
||||||
/// A track device slot is selected.
|
|
||||||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "track")]
|
|
||||||
impl Selection {
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
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<usize> {
|
|
||||||
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<str> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,540 +0,0 @@
|
||||||
//! MIDI sequencer
|
|
||||||
//! ```
|
|
||||||
//! let clip = tek_device::MidiClip::default();
|
|
||||||
//! println!("Empty clip: {clip:?}");
|
|
||||||
//!
|
|
||||||
//! let clip = tek_device::MidiClip::stop_all();
|
|
||||||
//! println!("Panic clip: {clip:?}");
|
|
||||||
//!
|
|
||||||
//! let mut clip = tek_device::MidiClip::new("clip", true, 1, None, None);
|
|
||||||
//! clip.set_length(96);
|
|
||||||
//! clip.toggle_loop();
|
|
||||||
//! clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
|
||||||
//! assert!(clip.contains_note_on(36.into(), 6, 18));
|
|
||||||
//! assert_eq!(&clip.notes, &clip.duplicate().notes);
|
|
||||||
//!
|
|
||||||
//! let clip = std::sync::Arc::new(clip);
|
|
||||||
//! assert_eq!(clip.clone(), clip);
|
|
||||||
//!
|
|
||||||
//! let sequencer = tek_device::Sequencer::default();
|
|
||||||
//! println!("{sequencer:?}");
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl<T: Has<Sequencer>> HasSequencer for T {
|
|
||||||
fn sequencer (&self) -> &Sequencer {
|
|
||||||
self.get()
|
|
||||||
}
|
|
||||||
fn sequencer_mut (&mut self) -> &mut Sequencer {
|
|
||||||
self.get_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasSequencer {
|
|
||||||
fn sequencer (&self) -> &Sequencer;
|
|
||||||
fn sequencer_mut (&mut self) -> &mut Sequencer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains state for playing a clip
|
|
||||||
pub struct Sequencer {
|
|
||||||
/// State of clock and playhead
|
|
||||||
#[cfg(feature = "clock")] pub clock: Clock,
|
|
||||||
/// Start time and clip being played
|
|
||||||
#[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
|
||||||
/// Start time and next clip
|
|
||||||
#[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
|
||||||
/// Record from MIDI ports to current sequence.
|
|
||||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
|
||||||
/// Play from current sequence to MIDI ports
|
|
||||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
|
||||||
/// Play input through output.
|
|
||||||
pub monitoring: bool,
|
|
||||||
/// Write input to sequence.
|
|
||||||
pub recording: bool,
|
|
||||||
/// Overdub input to sequence.
|
|
||||||
pub overdub: bool,
|
|
||||||
/// Send all notes off
|
|
||||||
pub reset: bool, // TODO?: after Some(nframes)
|
|
||||||
/// Notes currently held at input
|
|
||||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// Notes currently held at output
|
|
||||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub note_buf: Vec<u8>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<str>,
|
|
||||||
jack: &Jack<'static>,
|
|
||||||
#[cfg(feature = "clock")] clock: Option<&Clock>,
|
|
||||||
#[cfg(feature = "clip")] clip: Option<&Arc<RwLock<MidiClip>>>,
|
|
||||||
#[cfg(feature = "port")] midi_from: &[Connect],
|
|
||||||
#[cfg(feature = "port")] midi_to: &[Connect],
|
|
||||||
) -> Usually<Self> {
|
|
||||||
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<MidiInput>: |self:Sequencer|self.midi_ins);
|
|
||||||
#[cfg(feature = "port")] has!(Vec<MidiOutput>: |self:Sequencer|self.midi_outs);
|
|
||||||
|
|
||||||
impl MidiMonitor for Sequencer {
|
|
||||||
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
|
|
||||||
&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<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&self.play_clip
|
|
||||||
}
|
|
||||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&mut self.play_clip
|
|
||||||
}
|
|
||||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&self.next_clip
|
|
||||||
}
|
|
||||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasMidiBuffers {
|
|
||||||
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
|
|
||||||
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasMidiBuffers for Sequencer {
|
|
||||||
fn note_buf_mut (&mut self) -> &mut Vec<u8> {
|
|
||||||
&mut self.note_buf
|
|
||||||
}
|
|
||||||
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>> {
|
|
||||||
&mut self.midi_buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiMonitor: HasMidiIns + HasMidiBuffers {
|
|
||||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
|
||||||
fn monitoring (&self) -> bool;
|
|
||||||
fn monitoring_mut (&mut self) -> &mut bool;
|
|
||||||
fn toggle_monitor (&mut self) {
|
|
||||||
*self.monitoring_mut() = !self.monitoring();
|
|
||||||
}
|
|
||||||
fn monitor (&mut self, scope: &ProcessScope) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
|
|
||||||
fn recording (&self) -> bool;
|
|
||||||
fn recording_mut (&mut self) -> &mut bool;
|
|
||||||
fn toggle_record (&mut self) {
|
|
||||||
*self.recording_mut() = !self.recording();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overdub (&self) -> bool;
|
|
||||||
fn overdub_mut (&mut self) -> &mut bool;
|
|
||||||
fn toggle_overdub (&mut self) {
|
|
||||||
*self.overdub_mut() = !self.overdub();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_clip (
|
|
||||||
&mut self,
|
|
||||||
scope: &ProcessScope,
|
|
||||||
started: Moment,
|
|
||||||
clip: &Option<Arc<RwLock<MidiClip>>>,
|
|
||||||
) {
|
|
||||||
if let Some(clip) = clip {
|
|
||||||
let sample0 = scope.last_frame_time() as usize;
|
|
||||||
let start = started.sample.get() as usize;
|
|
||||||
let _recording = self.recording();
|
|
||||||
let timebase = self.clock().timebase().clone();
|
|
||||||
let quant = self.clock().quant.get();
|
|
||||||
let mut clip = clip.write().unwrap();
|
|
||||||
let length = clip.length;
|
|
||||||
for input in self.midi_ins_mut().iter() {
|
|
||||||
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
|
||||||
if let LiveEvent::Midi { message, .. } = event {
|
|
||||||
clip.record_event({
|
|
||||||
let sample = (sample0 + sample - start) as f64;
|
|
||||||
let pulse = timebase.samples_to_pulse(sample);
|
|
||||||
let quantized = (pulse / quant).round() * quant;
|
|
||||||
quantized as usize % length
|
|
||||||
}, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_next (&mut self) {
|
|
||||||
// TODO switch to next clip and record into it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiViewer: Measured<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
|
|
||||||
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
|
|
||||||
fn redraw (&self);
|
|
||||||
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
|
||||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
|
||||||
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
||||||
*self.clip_mut() = clip.cloned();
|
|
||||||
self.redraw();
|
|
||||||
}
|
|
||||||
/// Make sure cursor is within note range
|
|
||||||
fn autoscroll (&self) {
|
|
||||||
let note_pos = self.get_note_pos().min(127);
|
|
||||||
let note_lo = self.get_note_lo();
|
|
||||||
let note_hi = self.get_note_hi();
|
|
||||||
if note_pos < note_lo {
|
|
||||||
self.note_lo().set(note_pos);
|
|
||||||
} else if note_pos > note_hi {
|
|
||||||
self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Make sure time range is within display
|
|
||||||
fn autozoom (&self) {
|
|
||||||
if self.time_lock().get() {
|
|
||||||
let time_len = self.get_time_len();
|
|
||||||
let time_axis = self.get_time_axis();
|
|
||||||
let time_zoom = self.get_time_zoom();
|
|
||||||
loop {
|
|
||||||
let time_zoom = self.time_zoom().get();
|
|
||||||
let time_area = time_axis * time_zoom;
|
|
||||||
if time_area > time_len {
|
|
||||||
let next_time_zoom = note_duration_prev(time_zoom);
|
|
||||||
if next_time_zoom <= 1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let next_time_area = time_axis * next_time_zoom;
|
|
||||||
if next_time_area >= time_len {
|
|
||||||
self.time_zoom().set(next_time_zoom);
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if time_area < time_len {
|
|
||||||
let prev_time_zoom = note_duration_next(time_zoom);
|
|
||||||
if prev_time_zoom > 384 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let prev_time_area = time_axis * prev_time_zoom;
|
|
||||||
if prev_time_area <= time_len {
|
|
||||||
self.time_zoom().set(prev_time_zoom);
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if time_zoom != self.time_zoom().get() {
|
|
||||||
self.redraw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//while time_len.div_ceil(time_zoom) > time_axis {
|
|
||||||
//println!("\r{time_len} {time_zoom} {time_axis}");
|
|
||||||
//time_zoom = Note::next(time_zoom);
|
|
||||||
//}
|
|
||||||
//self.time_zoom().set(time_zoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasPlayClip: HasClock {
|
|
||||||
|
|
||||||
fn reset (&self) -> bool;
|
|
||||||
|
|
||||||
fn reset_mut (&mut self) -> &mut bool;
|
|
||||||
|
|
||||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
||||||
|
|
||||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
||||||
|
|
||||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
||||||
|
|
||||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
||||||
|
|
||||||
fn pulses_since_start (&self) -> Option<f64> {
|
|
||||||
if let Some((started, Some(_))) = self.play_clip().as_ref() {
|
|
||||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
||||||
return Some(elapsed)
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
|
||||||
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
|
|
||||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
||||||
let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
|
|
||||||
let times = (elapsed as usize / length) as f64;
|
|
||||||
let elapsed = (elapsed as usize % length) as f64;
|
|
||||||
return Some((times, elapsed))
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
||||||
*self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned()));
|
|
||||||
*self.reset_mut() = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_status (&self) -> impl Content<TuiOut> {
|
|
||||||
let (name, color): (Arc<str>, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() {
|
|
||||||
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
|
||||||
(name.clone(), color)
|
|
||||||
} else {
|
|
||||||
("".into(), Tui::g(64).into())
|
|
||||||
};
|
|
||||||
let time: String = self.pulses_since_start_looped()
|
|
||||||
.map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time)))
|
|
||||||
.unwrap_or_else(||String::from(" ")).into();
|
|
||||||
FieldV(color, "Now:", format!("{} {}", time, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_status (&self) -> impl Content<TuiOut> {
|
|
||||||
let mut time: Arc<str> = String::from("--.-.--").into();
|
|
||||||
let mut name: Arc<str> = String::from("").into();
|
|
||||||
let mut color = ItemTheme::G[64];
|
|
||||||
let clock = self.clock();
|
|
||||||
if let Some((t, Some(clip))) = self.next_clip() {
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
name = clip.name.clone();
|
|
||||||
color = clip.color.clone();
|
|
||||||
time = {
|
|
||||||
let target = t.pulse.get();
|
|
||||||
let current = clock.playhead.pulse.get();
|
|
||||||
if target > current {
|
|
||||||
let remaining = target - current;
|
|
||||||
format!("-{:>}", clock.timebase.format_beats_1(remaining))
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}.into()
|
|
||||||
} else if let Some((t, Some(clip))) = self.play_clip() {
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
if clip.looped {
|
|
||||||
name = clip.name.clone();
|
|
||||||
color = clip.color.clone();
|
|
||||||
let target = t.pulse.get() + clip.length as f64;
|
|
||||||
let current = clock.playhead.pulse.get();
|
|
||||||
if target > current {
|
|
||||||
time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
name = "Stop".to_string().into();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
FieldV(color, "Next:", format!("{} {}", time, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
369
device/track.rs
369
device/track.rs
|
|
@ -1,369 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let track: tek_device::Track = Default::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Track {
|
|
||||||
/// Name of track
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Identifying color of track
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Preferred width of track column
|
|
||||||
pub width: usize,
|
|
||||||
/// MIDI sequencer state
|
|
||||||
pub sequencer: Sequencer,
|
|
||||||
/// Device chain
|
|
||||||
pub devices: Vec<Device>,
|
|
||||||
}
|
|
||||||
|
|
||||||
def_sizes_iter!(TracksSizes => Track);
|
|
||||||
|
|
||||||
impl<T: Has<Vec<Track>> + Send + Sync> HasTracks for T {}
|
|
||||||
|
|
||||||
pub trait HasTracks: Has<Vec<Track>> + Send + Sync {
|
|
||||||
fn tracks (&self) -> &Vec<Track> { Has::<Vec<Track>>::get(self) }
|
|
||||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { Has::<Vec<Track>>::get_mut(self) }
|
|
||||||
/// Run audio callbacks for every track and every device
|
|
||||||
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
for track in self.tracks_mut().iter_mut() {
|
|
||||||
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
for device in track.devices.iter_mut() {
|
|
||||||
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
|
||||||
/// Stop all playing clips
|
|
||||||
fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } }
|
|
||||||
/// Stop all playing clips
|
|
||||||
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
|
||||||
if let Some(clips) = clips {
|
|
||||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); }
|
|
||||||
} else {
|
|
||||||
for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Spacing between tracks.
|
|
||||||
const TRACK_SPACING: usize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasTrackScroll: HasTracks {
|
|
||||||
fn track_scroll (&self) -> usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasTrackScroll for Arrangement {
|
|
||||||
fn track_scroll (&self) -> usize {
|
|
||||||
self.track_scroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasTrack {
|
|
||||||
fn track (&self) -> Option<&Track>;
|
|
||||||
fn track_mut (&mut self) -> Option<&mut Track>;
|
|
||||||
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + 'a {
|
|
||||||
self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
|
||||||
}
|
|
||||||
#[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> + '_ {
|
|
||||||
self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
|
||||||
}
|
|
||||||
#[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
||||||
self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
|
||||||
}
|
|
||||||
#[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
||||||
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: MaybeHas<Track>> 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<str>,
|
|
||||||
color: Option<ItemTheme>,
|
|
||||||
jack: &Jack<'static>,
|
|
||||||
clock: Option<&Clock>,
|
|
||||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
|
||||||
midi_from: &[Connect],
|
|
||||||
midi_to: &[Connect],
|
|
||||||
) -> Usually<Self> {
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn audio_ins (&self) -> &[AudioInput] {
|
|
||||||
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
fn audio_outs (&self) -> &[AudioOutput] {
|
|
||||||
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
|
||||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
|
||||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { 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<str>,
|
|
||||||
color: Option<ItemTheme>,
|
|
||||||
jack: &Jack<'static>,
|
|
||||||
clock: Option<&Clock>,
|
|
||||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
|
||||||
midi_from: &[Connect],
|
|
||||||
midi_to: &[Connect],
|
|
||||||
audio_from: &[&[Connect];2],
|
|
||||||
audio_to: &[&[Connect];2],
|
|
||||||
) -> Usually<Self> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def_command!(TrackCommand: |track: Track| {
|
|
||||||
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
|
||||||
SetMute { mute: Option<bool> } => todo!(),
|
|
||||||
SetSolo { solo: Option<bool> } => todo!(),
|
|
||||||
SetSize { size: usize } => todo!(),
|
|
||||||
SetZoom { zoom: usize } => todo!(),
|
|
||||||
SetName { name: Arc<str> } =>
|
|
||||||
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
|
||||||
SetColor { color: ItemTheme } =>
|
|
||||||
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
|
||||||
SetRec { rec: Option<bool> } =>
|
|
||||||
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
|
||||||
SetMon { mon: Option<bool> } =>
|
|
||||||
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
|
||||||
});
|
|
||||||
|
|
||||||
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
|
|
||||||
|
|
||||||
pub trait TracksView:
|
|
||||||
ScenesView +
|
|
||||||
HasMidiIns +
|
|
||||||
HasMidiOuts +
|
|
||||||
Measured<TuiOut> +
|
|
||||||
HasTrackScroll +
|
|
||||||
HasSelection +
|
|
||||||
HasEditor +
|
|
||||||
HasClipsSize
|
|
||||||
{
|
|
||||||
fn tracks_width_available (&self) -> u16 {
|
|
||||||
(self.measure_width() as u16).saturating_sub(40)
|
|
||||||
}
|
|
||||||
/// Iterate over tracks with their corresponding sizes.
|
|
||||||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
|
||||||
let _editor_width = self.editor().map(|e|e.measure_width());
|
|
||||||
let _active_track = self.selection().track();
|
|
||||||
let mut x = 0;
|
|
||||||
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
|
||||||
let width = track.width.max(8);
|
|
||||||
if x + width < self.clips_size().w() as usize {
|
|
||||||
let data = (index, track, x, x + width);
|
|
||||||
x += width + Self::TRACK_SPACING;
|
|
||||||
Some(data)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
||||||
let track_count = self.tracks().len();
|
|
||||||
let scene_count = self.scenes().len();
|
|
||||||
let selected = self.selection();
|
|
||||||
let button = Bsp::s(
|
|
||||||
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
|
||||||
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
|
||||||
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
|
||||||
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
|
||||||
let button_2 = Bsp::s(
|
|
||||||
button_2("T", "+", false),
|
|
||||||
button_2("S", "+", false));
|
|
||||||
view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb,
|
|
||||||
Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{
|
|
||||||
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
|
||||||
to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track),
|
|
||||||
Tui::bg(if selected.track() == Some(index) {
|
|
||||||
track.color.light.rgb
|
|
||||||
} else {
|
|
||||||
track.color.base.rgb
|
|
||||||
}, Bsp::s(Fill::X(Align::nw(Bsp::e(
|
|
||||||
format!("·t{index:02} "),
|
|
||||||
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name))
|
|
||||||
))), ""))) ));}}))))
|
|
||||||
}
|
|
||||||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content<TuiOut> {
|
|
||||||
view_track_row_section(theme,
|
|
||||||
Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))),
|
|
||||||
Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() {
|
|
||||||
to.place(&Fill::X(Align::w(port.port_name())));
|
|
||||||
})),
|
|
||||||
button_2("O", "+", false),
|
|
||||||
Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{
|
|
||||||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
||||||
to.place(&Fixed::X(track_width(index, track),
|
|
||||||
Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(),
|
|
||||||
|port, index|Tui::fg(Rgb(255, 255, 255),
|
|
||||||
Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w(
|
|
||||||
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
|
|
||||||
}
|
|
||||||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
||||||
let mut h = 0u16;
|
|
||||||
for track in self.tracks().iter() {
|
|
||||||
h = h.max(track.sequencer.midi_ins.len() as u16);
|
|
||||||
}
|
|
||||||
let content = 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,
|
|
||||||
Align::nw(Bsp::s(
|
|
||||||
Tui::bg(track.color.base.rgb,
|
|
||||||
Fill::X(Align::w(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 "),
|
|
||||||
)))),
|
|
||||||
Map::south(1, ||track.sequencer.midi_ins.iter(),
|
|
||||||
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
|
||||||
Fill::X(Align::w(format!("·i{index:02} {}", port.port_name())))))))));
|
|
||||||
});
|
|
||||||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
|
||||||
Tui::bg(theme.darker.rgb, Align::w(content)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
|
||||||
track.width as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_track_header (theme: ItemTheme, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
|
||||||
Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
|
||||||
-> impl Content<TuiOut> + use<'a, T>
|
|
||||||
{
|
|
||||||
let ins = ports.len() as u16;
|
|
||||||
let frame = Outer(true, Style::default().fg(Tui::g(96)));
|
|
||||||
let iter = move||ports.iter();
|
|
||||||
let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name()))));
|
|
||||||
let field = FieldV(theme, title, names);
|
|
||||||
Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field)))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Track {
|
|
||||||
pub fn per <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
|
||||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
||||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
||||||
) -> impl Content<TuiOut> + '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))))})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
|
||||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
||||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
||||||
) -> impl Content<TuiOut> + 'a {
|
|
||||||
Align::x(Tui::bg(Reset, 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))))})))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
|
||||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
|
||||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
|
||||||
) -> impl Content<TuiOut> + 'a {
|
|
||||||
per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track))))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
|
||||||
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
|
||||||
) -> impl Content<TuiOut> + 'a {
|
|
||||||
Map::new(iter, move|(
|
|
||||||
_index, name, connections, y, y2
|
|
||||||
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
|
|
||||||
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
|
||||||
Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ", name))))),
|
|
||||||
Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1,
|
|
||||||
Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
|
||||||
&connect.info)))))))))
|
|
||||||
}
|
|
||||||
|
|
||||||
//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
|
||||||
//fg: Color, bg: Color, iter: &mut impl Iterator<Item = (usize, &'a Arc<str>, &'a [Connect], usize, usize)>
|
|
||||||
//) -> impl Content<TuiOut> + 'a {
|
|
||||||
//Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter {
|
|
||||||
//to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s(
|
|
||||||
//Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"▞▞▞▞ ▞▞▞▞"))))),
|
|
||||||
//Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() {
|
|
||||||
//to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false,
|
|
||||||
//wrap(bg, fg, Fill::Y(&"")))))))
|
|
||||||
//})
|
|
||||||
//)))
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH {
|
|
||||||
//offset: arrangement.track_scroll,
|
|
||||||
//length: h_tracks_area as usize,
|
|
||||||
//total: h_scenes as usize,
|
|
||||||
//})),
|
|
||||||
//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref()
|
|
||||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
|
||||||
|
|
||||||
fn set_vst_plugin <E: Engine> (host: &Arc<Mutex<Plugin<E>>>, _path: &str) -> Usually<PluginKind> {
|
|
||||||
let mut loader = ::vst::host::PluginLoader::load(
|
|
||||||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
|
||||||
host.clone()
|
|
||||||
)?;
|
|
||||||
Ok(PluginKind::VST2 {
|
|
||||||
instance: loader.instance()?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
struct Vst3; // TODO
|
|
||||||
2
dizzle
2
dizzle
|
|
@ -1 +1 @@
|
||||||
Subproject commit be9503a119722a3b3573563c4bae2b0d52978bcd
|
Subproject commit 624ac97274fbf974f1196864bb93093c31cb0fd7
|
||||||
2
tengri
2
tengri
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7a8365c0174ddee65bd15ccca5dd7b03f110e002
|
Subproject commit b4ebdb8ff80941f45647b628ac2d7b21969f3241
|
||||||
Loading…
Add table
Add a link
Reference in a new issue