mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 16:29:04 +01:00
refactor: into one big pile it goes
This commit is contained in:
parent
604a42a4bc
commit
ec404b0305
24 changed files with 6559 additions and 6675 deletions
614
app/.scratch.rs
614
app/.scratch.rs
|
|
@ -377,3 +377,617 @@
|
|||
|
||||
|
||||
|
||||
//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()));
|
||||
|
|
|
|||
|
|
@ -14,48 +14,50 @@ path = "tek.rs"
|
|||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[dependencies]
|
||||
tek_device = { path = "../device" }
|
||||
|
||||
atomic_float = { workspace = true }
|
||||
backtrace = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
jack = { workspace = true }
|
||||
konst = { workspace = true }
|
||||
livi = { workspace = true, optional = true }
|
||||
midly = { workspace = true }
|
||||
palette = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
symphonia = { workspace = true, optional = true }
|
||||
tengri = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
uuid = { workspace = true, optional = true }
|
||||
wavers = { workspace = true, optional = true }
|
||||
winit = { workspace = true, optional = true }
|
||||
xdg = { workspace = true }
|
||||
ansi_term = "0.12.1"
|
||||
atomic_float = { workspace = true }
|
||||
backtrace = { workspace = true }
|
||||
builder-pattern = "0.4.2"
|
||||
clap = { workspace = true, optional = true }
|
||||
jack = { workspace = true }
|
||||
konst = { workspace = true }
|
||||
livi = { workspace = true, optional = true }
|
||||
midly = { workspace = true }
|
||||
palette = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
symphonia = { workspace = true, optional = true }
|
||||
tengri = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
uuid = { workspace = true, optional = true }
|
||||
wavers = { workspace = true, optional = true }
|
||||
winit = { workspace = true, optional = true }
|
||||
xdg = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
proptest-derive = { workspace = true }
|
||||
tek_engine = { path = "../engine" }
|
||||
|
||||
[features]
|
||||
arranger = ["port", "editor", "sequencer", "editor"]
|
||||
arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"]
|
||||
browse = []
|
||||
clap = []
|
||||
cli = ["dep:clap"]
|
||||
clip = []
|
||||
clock = []
|
||||
default = ["cli", "arranger", "sampler", "lv2"]
|
||||
editor = []
|
||||
host = ["lv2"]
|
||||
lv2 = ["port", "livi", "winit"]
|
||||
lv2 = ["port", "livi"]
|
||||
lv2_gui = ["lv2", "winit"]
|
||||
meter = []
|
||||
mixer = []
|
||||
pool = []
|
||||
port = []
|
||||
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
||||
scene = []
|
||||
select = []
|
||||
sequencer = ["port", "clock", "uuid", "pool"]
|
||||
sf2 = []
|
||||
track = []
|
||||
vst2 = []
|
||||
vst3 = []
|
||||
|
|
|
|||
1095
app/tek.rs
1095
app/tek.rs
File diff suppressed because it is too large
Load diff
3741
app/tek_impls.rs
3741
app/tek_impls.rs
File diff suppressed because it is too large
Load diff
|
|
@ -2,12 +2,64 @@ use crate::*;
|
|||
use clap::{self, Parser, Subcommand};
|
||||
use builder_pattern::Builder;
|
||||
|
||||
/// Wraps [JackState], and through it [jack::Client] when connected.
|
||||
///
|
||||
/// ```
|
||||
/// let jack = tek::Jack::default();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)] pub struct Jack<'j> (
|
||||
pub(crate) Arc<RwLock<JackState<'j>>>
|
||||
);
|
||||
|
||||
/// This is a connection which may be [Inactive], [Activating], or [Active].
|
||||
/// In the [Active] and [Inactive] states, [JackState::client] returns a
|
||||
/// [jack::Client], which you can use to talk to the JACK API.
|
||||
///
|
||||
/// ```
|
||||
/// let state = tek::JackState::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub enum JackState<'j> {
|
||||
/// Unused
|
||||
#[default] Inert,
|
||||
/// Before activation.
|
||||
Inactive(Client),
|
||||
/// During activation.
|
||||
Activating,
|
||||
/// After activation. Must not be dropped for JACK thread to persist.
|
||||
Active(DynamicAsyncClient<'j>),
|
||||
}
|
||||
|
||||
/// Event enum for JACK events.
|
||||
///
|
||||
/// ```
|
||||
/// let event = tek::JackEvent::XRun;
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
||||
ThreadInit,
|
||||
Shutdown(ClientStatus, Arc<str>),
|
||||
Freewheel(bool),
|
||||
SampleRate(Frames),
|
||||
ClientRegistration(Arc<str>, bool),
|
||||
PortRegistration(PortId, bool),
|
||||
PortRename(PortId, Arc<str>, Arc<str>),
|
||||
PortsConnected(PortId, PortId, bool),
|
||||
GraphReorder,
|
||||
XRun,
|
||||
}
|
||||
|
||||
/// Generic notification handler that emits [JackEvent]
|
||||
///
|
||||
/// ```
|
||||
/// let notify = tek::JackNotify(|_|{});
|
||||
/// ```
|
||||
pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||
|
||||
/// Total state
|
||||
///
|
||||
/// ```
|
||||
/// use tek::tek_device::TracksView;
|
||||
/// let app: tek::App = Default::default();
|
||||
/// let _ = app.scene_add(None, None)?;
|
||||
/// use tek::{TracksView, ScenesView, AddScene};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scene_add(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
|
|
@ -53,7 +105,7 @@ use builder_pattern::Builder;
|
|||
/// Configuration: mode, view, and bind definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let conf: tek::Config = Default::default();
|
||||
/// let conf = tek::Config::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Config {
|
||||
/// XDG base directories of running user.
|
||||
|
|
@ -69,7 +121,7 @@ use builder_pattern::Builder;
|
|||
/// Group of view and keys definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
||||
pub path: PathBuf,
|
||||
|
|
@ -83,7 +135,7 @@ use builder_pattern::Builder;
|
|||
/// An input binding.
|
||||
///
|
||||
/// ```
|
||||
/// let bind: tek::Bind<(), ()> = Default::default();
|
||||
/// let bind = tek::Bind::<(), ()>::default();
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct Bind<E, C>(
|
||||
/// Map of each event (e.g. key combination) to
|
||||
|
|
@ -142,9 +194,9 @@ use builder_pattern::Builder;
|
|||
/// use clap::CommandFactory;
|
||||
/// tek::Cli::command().debug_assert();
|
||||
/// ```
|
||||
#[derive(Debug, Parser, Default)]
|
||||
#[derive(Parser)]
|
||||
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
||||
pub struct Cli {
|
||||
#[derive(Debug, Default)] pub struct Cli {
|
||||
/// Pre-defined configuration modes.
|
||||
///
|
||||
/// TODO: Replace these with scripted configurations.
|
||||
|
|
@ -213,8 +265,7 @@ pub struct Cli {
|
|||
/// ```
|
||||
/// let axis = tek::ControlAxis::X;
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ControlAxis {
|
||||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||||
X, Y, Z, I
|
||||
}
|
||||
|
||||
|
|
@ -232,3 +283,794 @@ pub enum ControlAxis {
|
|||
Browse(BrowseTarget, Arc<Browse>),
|
||||
Options,
|
||||
}
|
||||
|
||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct Timebase {
|
||||
/// Audio samples per second
|
||||
pub sr: SampleRate,
|
||||
/// MIDI beats per minute
|
||||
pub bpm: BeatsPerMinute,
|
||||
/// MIDI ticks per beat
|
||||
pub ppq: PulsesPerQuaver,
|
||||
}
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
///
|
||||
/// ```
|
||||
/// let iter = tek::TicksIterator::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct TicksIterator {
|
||||
pub spp: f64,
|
||||
pub sample: usize,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{TimeRange, NoteRange};
|
||||
/// let model = tek::MidiSelection::from((1, false));
|
||||
///
|
||||
/// let _ = model.get_time_len();
|
||||
/// let _ = model.get_time_zoom();
|
||||
/// let _ = model.get_time_lock();
|
||||
/// let _ = model.get_time_start();
|
||||
/// let _ = model.get_time_axis();
|
||||
/// let _ = model.get_time_end();
|
||||
///
|
||||
/// let _ = model.get_note_lo();
|
||||
/// let _ = model.get_note_axis();
|
||||
/// let _ = model.get_note_hi();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct MidiCursor {
|
||||
/// Time coordinate of cursor
|
||||
pub time_pos: Arc<AtomicUsize>,
|
||||
/// Note coordinate of cursor
|
||||
pub note_pos: Arc<AtomicUsize>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub note_len: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
|
||||
pub time_len: Arc<AtomicUsize>,
|
||||
/// Length of visible time axis
|
||||
pub time_axis: Arc<AtomicUsize>,
|
||||
/// Earliest time displayed
|
||||
pub time_start: Arc<AtomicUsize>,
|
||||
/// Time step
|
||||
pub time_zoom: Arc<AtomicUsize>,
|
||||
/// Auto rezoom to fit in time axis
|
||||
pub time_lock: Arc<AtomicBool>,
|
||||
/// Length of visible note axis
|
||||
pub note_axis: Arc<AtomicUsize>,
|
||||
// Lowest note displayed
|
||||
pub note_lo: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)] pub struct Moment {
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current time in microseconds
|
||||
pub usec: Microsecond,
|
||||
/// Current time in audio samples
|
||||
pub sample: SampleCount,
|
||||
/// Current time in MIDI pulses
|
||||
pub pulse: Pulse,
|
||||
}
|
||||
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
||||
#[default] None,
|
||||
Zero,
|
||||
Usec(Microsecond),
|
||||
Sample(SampleCount),
|
||||
Pulse(Pulse),
|
||||
}
|
||||
|
||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct PulsesPerQuaver (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in MIDI pulses
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
||||
|
||||
/// Tempo in beats per minute
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct BeatsPerMinute (pub(crate) AtomicF64);
|
||||
|
||||
/// Quantization setting for launching clips
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64);
|
||||
|
||||
/// Quantization setting for notes
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in audio samples
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
||||
|
||||
/// Audio sample rate in Hz (samples per second)
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
||||
|
||||
/// Timestamp in microseconds
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
||||
|
||||
/// The source of time.
|
||||
///
|
||||
/// ```
|
||||
/// let clock = tek::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::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 files to load/save.
|
||||
///
|
||||
/// ```
|
||||
/// let browse = tek::Browse::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>,
|
||||
}
|
||||
|
||||
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::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::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.
|
||||
///
|
||||
/// ```
|
||||
/// use std::sync::{Arc, RwLock};
|
||||
/// let clip = tek::MidiClip::stop_all();
|
||||
/// let mut editor = tek::MidiEditor {
|
||||
/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))),
|
||||
/// 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::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::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::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::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::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 samples: Samples<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),
|
||||
}
|
||||
|
||||
/// Collection of samples, one per slot, fixed number of slots.
|
||||
///
|
||||
/// TODO: Map more than one sample per slot.
|
||||
///
|
||||
/// History: Separated to cleanly implement [Default].
|
||||
///
|
||||
/// ```
|
||||
/// let samples = tek::Samples([None, None, None, None]);
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct Samples<const N: usize>(
|
||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||
);
|
||||
|
||||
/// A sound cut.
|
||||
///
|
||||
/// ```
|
||||
/// let sample = tek::Sample::default();
|
||||
/// let sample = tek::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::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::MidiClip::default();
|
||||
/// println!("Empty clip: {clip:?}");
|
||||
///
|
||||
/// let clip = tek::MidiClip::stop_all();
|
||||
/// println!("Panic clip: {clip:?}");
|
||||
///
|
||||
/// let mut clip = tek::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::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::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),
|
||||
}
|
||||
|
|
|
|||
1040
app/tek_trait.rs
Normal file
1040
app/tek_trait.rs
Normal file
File diff suppressed because it is too large
Load diff
51
app/tek_type.rs
Normal file
51
app/tek_type.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use crate::*;
|
||||
|
||||
/// Running JACK [AsyncClient] with maximum type erasure.
|
||||
///
|
||||
/// One [Box] contains function that handles [JackEvent]s.
|
||||
///
|
||||
/// Another [Box] containing a function that handles realtime IO.
|
||||
///
|
||||
/// That's all it knows about them.
|
||||
pub type DynamicAsyncClient<'j>
|
||||
= AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>;
|
||||
|
||||
/// Notification handler wrapper for [BoxedAudioHandler].
|
||||
pub type DynamicAudioHandler<'j> =
|
||||
ClosureProcessHandler<(), BoxedAudioHandler<'j>>;
|
||||
|
||||
/// Boxed realtime callback.
|
||||
pub type BoxedAudioHandler<'j> =
|
||||
Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send + Sync + 'j>;
|
||||
|
||||
/// Notification handler wrapper for [BoxedJackEventHandler].
|
||||
pub type DynamicNotifications<'j> =
|
||||
JackNotify<BoxedJackEventHandler<'j>>;
|
||||
|
||||
/// Boxed [JackEvent] callback.
|
||||
pub type BoxedJackEventHandler<'j> =
|
||||
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
|
||||
|
||||
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>>);
|
||||
|
||||
/// Collection of interaction modes.
|
||||
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
||||
|
||||
/// Collection of input bindings.
|
||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
||||
|
||||
/// Collection of view definitions.
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue