diff --git a/Cargo.lock b/Cargo.lock index 23cc9338..57262199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2642,8 +2642,6 @@ dependencies = [ "proptest-derive", "rand 0.8.5", "symphonia", - "tek_device", - "tek_engine", "tengri", "toml", "uuid", @@ -2652,43 +2650,6 @@ dependencies = [ "xdg", ] -[[package]] -name = "tek_device" -version = "0.3.0" -dependencies = [ - "atomic_float", - "backtrace", - "clap", - "jack", - "konst", - "livi", - "midly", - "palette", - "proptest", - "proptest-derive", - "rand 0.8.5", - "symphonia", - "tek_engine", - "tengri", - "toml", - "uuid", - "wavers", - "winit", - "xdg", -] - -[[package]] -name = "tek_engine" -version = "0.3.0" -dependencies = [ - "atomic_float", - "jack", - "midly", - "proptest", - "proptest-derive", - "tengri", -] - [[package]] name = "tempfile" version = "3.25.0" diff --git a/Cargo.toml b/Cargo.toml index 81945851..5b603f9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "./app", "./engine", "./device" ] +members = [ "./app" ] exclude = [ "./tengri", "./dizzle" ] [workspace.package] diff --git a/Justfile b/Justfile index eac680df..df3357d4 100644 --- a/Justfile +++ b/Justfile @@ -119,7 +119,6 @@ plugin: @stats: echo - for file in $(ls app/*.rs); do echo $file; cloc $file | grep Rust; done echo cloc app | grep Rust @@ -131,27 +130,3 @@ plugin: rg 'TODO' app/ | cat rg 'TODO' app/ | wc -l echo - - for file in $(ls engine/*.rs); do echo $file; cloc $file | grep Rust; done - echo - cloc engine | grep Rust - rg 'pub struct ' engine/ | cat - rg 'pub struct ' engine/ | wc -l - rg 'pub trait ' engine/ | cat - rg 'pub trait ' engine/ | wc -l - echo - rg 'TODO' engine/ | cat - rg 'TODO' engine/ | wc -l - echo - - for file in $(ls device/*.rs); do echo $file; cloc $file | grep Rust; done - echo - cloc device | grep Rust - rg 'pub struct ' device/ | cat - rg 'pub struct ' device/ | wc -l - rg 'pub trait ' device/ | cat - rg 'pub trait ' device/ | wc -l - echo - rg 'TODO' device/ | cat - rg 'TODO' device/ | wc -l - echo diff --git a/app/.scratch.rs b/app/.scratch.rs index b3e36a2f..922b4199 100644 --- a/app/.scratch.rs +++ b/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) => { + //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 } => { + //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 { + //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 { + //todo!() + //} + //fn file_browser_path (&self) -> PathBuf { + //todo!(); + //} + ///// Immutable reference to sample at cursor. + //fn sample_selected (&self) -> Option>> { + //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::Select(state.set_note_pos(i)) + //} + ///// Assign sample to slot + //set (&self, slot: u7, sample: Option>>) -> Option { + //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 { + //todo!() + //} + //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { + //todo!() + //} + //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { + //todo!() + //} + //note_off (&self, state: &mut Sampler, slot: u7) -> Option { + //todo!() + //} + //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { + //Some(Self::SetSample(p, state.set_sample(p, s))) + //} + //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { + //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>>] 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>>] + ////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, &'a [Connect], usize, usize)> +//) -> impl Content + '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())); diff --git a/app/Cargo.toml b/app/Cargo.toml index c1b01a53..975340ad 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -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 = [] diff --git a/app/tek.rs b/app/tek.rs index 8a910220..54b0e33a 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -7,20 +7,38 @@ trait_alias, type_alias_impl_trait, type_changing_struct_update)] + #[cfg(test)] mod tek_test; -#[allow(unused)] pub(crate) use ::std::{ - cmp::Ord, - collections::BTreeMap, - error::Error, - ffi::OsString, - fmt::{Write, Debug, Formatter}, - fs::File, - ops::{Add, Sub, Mul, Div, Rem}, - path::{Path, PathBuf}, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - thread::JoinHandle, -}; + +mod tek_struct; +pub use self::tek_struct::*; + +mod tek_trait; +pub use self::tek_trait::*; + +mod tek_type; +pub use self::tek_type::*; + +mod tek_impls; + +pub(crate) use ConnectName::*; +pub(crate) use ConnectScope::*; +pub(crate) use ConnectStatus::*; +pub(crate) use JackState::*; + #[allow(unused)] pub(crate) use ::{ + std::{ + cmp::Ord, + collections::BTreeMap, + error::Error, + ffi::OsString, + fmt::{Write, Debug, Formatter}, + fs::File, + ops::{Add, Sub, Mul, Div, Rem}, + path::{Path, PathBuf}, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + thread::{spawn, JoinHandle}, + }, xdg::BaseDirectories, atomic_float::*, tengri::tui::ratatui::{ @@ -34,11 +52,37 @@ }, }; -pub extern crate tengri; pub(crate) use tengri::{*, input::*, output::*, tui::*}; -pub extern crate tek_device; pub(crate) use tek_device::{*, tek_engine::*}; +pub extern crate tengri; +pub(crate) use tengri::{*, input::*, output::*, tui::*}; -mod tek_struct; pub use self::tek_struct::*; -mod tek_impls; +pub extern crate jack; +pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; + +pub extern crate midly; +pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; + +pub extern crate atomic_float; +pub(crate) use atomic_float::AtomicF64; + +#[cfg(feature = "sampler")] pub(crate) use symphonia::{ + core::{ + formats::Packet, + codecs::{Decoder, CODEC_TYPE_NULL}, + //errors::Error as SymphoniaError, + io::MediaSourceStream, + probe::Hint, + audio::SampleBuffer, + }, + default::get_codecs, +}; + +#[cfg(feature = "lv2_gui")] use ::winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + window::{Window, WindowId}, + platform::x11::EventLoopBuilderExtX11 +}; /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { @@ -49,7 +93,7 @@ mod tek_impls; /// Create a new application from a backend, project, config, and mode /// /// ``` -/// let jack = tek_engine::Jack::new(&"test_tek").expect("failed to connect to jack"); +/// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let proj = Default::default(); /// let conf = Default::default(); /// let tek = tek::tek(&jack, proj, conf, ""); @@ -68,25 +112,6 @@ pub fn tek ( } } -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -/// Collection of input bindings. -pub type Binds = Arc, Bind>>>>; - -/// Collection of view definitions. -pub type Views = Arc, Arc>>>; - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => tek_confirm(app), - Cancel => todo!(), // TODO delegate: - Inc { axis: ControlAxis } => tek_inc(app, axis), - Dec { axis: ControlAxis } => tek_dec(app, axis), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); fn tek_confirm (state: &mut App) -> Perhaps { Ok(match &state.dialog { @@ -163,15 +188,6 @@ pub(crate) const HEADER: &'static str = r#" █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; -fn view_logo () -> impl Content { - Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::Y(1, ""), - Fixed::Y(1, ""), - Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) -} fn collect_commands ( app: &App, input: &TuiIn ) -> Usually> { @@ -330,3 +346,992 @@ pub fn tek_print_status (project: &Arrangement) { // TODO git integration // TODO dawvert integration } + +pub const DEFAULT_PPQ: f64 = 96.0; + +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// (pulses, name), assuming 96 PPQ +pub const NOTE_DURATIONS: [(usize, &str);26] = [ + (1, "1/384"), (2, "1/192"), + (3, "1/128"), (4, "1/96"), + (6, "1/64"), (8, "1/48"), + (12, "1/32"), (16, "1/24"), + (24, "1/16"), (32, "1/12"), + (48, "1/8"), (64, "1/6"), + (96, "1/4"), (128, "1/3"), + (192, "1/2"), (256, "2/3"), + (384, "1/1"), (512, "4/3"), + (576, "3/2"), (768, "2/1"), + (1152, "3/1"), (1536, "4/1"), + (2304, "6/1"), (3072, "8/1"), + (3456, "9/1"), (6144, "16/1"), +]; + +pub const NOTE_NAMES: [&str; 128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; + +/// Return boxed iterator of MIDI events +pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) + -> Box, &'a [u8])> + 'a> +{ + Box::new(input.map(|::jack::RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} + +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); +} + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} + +/// Returns the next shorter length +pub fn note_duration_prev (pulses: usize) -> usize { + for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } + pulses +} + +/// Returns the next longer length +pub fn note_duration_next (pulses: usize) -> usize { + for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } + pulses +} + +pub fn note_duration_to_name (pulses: usize) -> &'static str { + for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } + "" +} + +pub fn note_pitch_to_name (n: usize) -> &'static str { + if n > 127 { + panic!("to_note_name({n}): must be 0-127"); + } + NOTE_NAMES[n] +} + + +//macro_rules! impl_port { + //($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { + //#[derive(Debug)] pub struct $Name { + ///// Handle to JACK client, for receiving reconnect events. + //jack: Jack<'static>, + ///// Port name + //name: Arc, + ///// Port handle. + //port: Port<$Spec>, + ///// List of ports to connect to. + //conn: Vec + //} + //impl AsRef> for $Name { + //fn as_ref (&self) -> &Port<$Spec> { &self.port } + //} + //impl $Name { + //pub fn new ($jack: &Jack, name: impl AsRef, connect: &[PortConnect]) + //-> Usually + //{ + //let $name = name.as_ref(); + //let jack = $jack.clone(); + //let port = $port?; + //let name = $name.into(); + //let conn = connect.to_vec(); + //let port = Self { jack, port, name, conn }; + //port.connect_to_matching()?; + //Ok(port) + //} + //pub fn name (&self) -> &Arc { &self.name } + //pub fn port (&self) -> &Port<$Spec> { &self.port } + //pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port } + //pub fn into_port (self) -> Port<$Spec> { self.port } + //pub fn close (self) -> Usually<()> { + //let Self { jack, port, .. } = self; + //Ok(jack.with_client(|client|client.unregister_port(port))?) + //} + //} + //impl HasJack<'static> for $Name { + //fn jack (&self) -> &'static Jack<'static> { &self.jack } + //} + //impl JackPort<'static> for $Name { + //type Port = $Spec; + //type Pair = $Pair; + //fn port (&self) -> &Port<$Spec> { &self.port } + //} + //impl ConnectTo<'static, &str> for $Name { + //fn connect_to (&self, to: &str) -> Usually { + //self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { + //self.connect_to(port) + //} else { + //Ok(Missing) + //}) + //} + //} + //impl ConnectTo<'static, &Port> for $Name { + //fn connect_to (&self, port: &Port) -> Usually { + //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 + //})) + //} + //} + //impl ConnectTo<'static, &Port<$Pair>> for $Name { + //fn connect_to (&self, port: &Port<$Pair>) -> Usually { + //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 + //})) + //} + //} + //impl ConnectAuto<'static> for $Name { + //fn connections (&self) -> &[PortConnect] { + //&self.conn + //} + //} + //}; +//} + + + + +pub fn swap_value ( + target: &mut T, value: &T, returned: impl Fn(T)->U +) -> Perhaps { + if *target == *value { + Ok(None) + } else { + let mut value = value.clone(); + std::mem::swap(target, &mut value); + Ok(Some(returned(value))) + } +} + +pub fn toggle_bool ( + target: &mut bool, value: &Option, returned: impl Fn(Option)->U +) -> Perhaps { + let mut value = value.unwrap_or(!*target); + if value == *target { + Ok(None) + } else { + std::mem::swap(target, &mut value); + Ok(Some(returned(Some(value)))) + } +} + +pub fn device_kinds () -> &'static [&'static str] { + &[ + #[cfg(feature = "sampler")] "Sampler", + #[cfg(feature = "lv2")] "Plugin (LV2)", + ] +} + +impl>> HasDevices for T { + fn devices (&self) -> &Vec { + self.get() + } + fn devices_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +impl Device { + pub fn name (&self) -> &str { + match self { + Self::Sampler(sampler) => sampler.name.as_ref(), + _ => todo!(), + } + } + pub fn midi_ins (&self) -> &[MidiInput] { + match self { + //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], + _ => todo!() + } + } + pub fn midi_outs (&self) -> &[MidiOutput] { + match self { + Self::Sampler(_) => &[], + _ => todo!() + } + } + pub fn audio_ins (&self) -> &[AudioInput] { + match self { + Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), + _ => todo!() + } + } + pub fn audio_outs (&self) -> &[AudioOutput] { + match self { + Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), + _ => todo!() + } + } +} + +audio!(|self: DeviceAudio<'a>, client, scope|{ + use Device::*; + match self.0 { + Mute => { Control::Continue }, + Bypass => { /*TODO*/ Control::Continue }, + #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), + #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), + #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO + #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO + #[cfg(feature = "clap")] Clap => { todo!() }, // TODO + #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO + } +}); + +//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + + +#[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>> { $cb } + } + } +} + +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 +{ + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + + +#[cfg(feature = "lv2_gui")] +pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { + 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 ( + buffer: &mut [Vec], 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 ( + buffer: &mut [Vec], 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() +} + + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + 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)) +} + + + +pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { + track.width as u16 +} + + +#[cfg(feature = "vst2")] fn set_vst_plugin ( + host: &Arc>>, _path: &str +) -> Usually { + 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()? + }) +} + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => tek_confirm(app), + Cancel => todo!(), // TODO delegate: + Inc { axis: ControlAxis } => tek_inc(app, axis), + Dec { axis: ControlAxis } => tek_dec(app, axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +def_command!(ClockCommand: |clock: Clock| { + SeekUsec { usec: f64 } => { + clock.playhead.update_from_usec(*usec); Ok(None) }, + SeekSample { sample: f64 } => { + clock.playhead.update_from_sample(*sample); Ok(None) }, + SeekPulse { pulse: f64 } => { + clock.playhead.update_from_pulse(*pulse); Ok(None) }, + SetBpm { bpm: f64 } => Ok(Some( + Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), + SetQuant { quant: f64 } => Ok(Some( + Self::SetQuant { quant: clock.quant.set(*quant) })), + SetSync { sync: f64 } => Ok(Some( + Self::SetSync { sync: clock.sync.set(*sync) })), + + Play { position: Option } => { + clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, + Pause { position: Option } => { + clock.pause_at(*position)?; Ok(None) }, + + TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { + clock.pause_at(Some(*position))?; None + } else { + clock.play_from(Some(*position))?; None + }), +}); + +def_command!(DeviceCommand: |device: Device| {}); + +def_command!(ClipCommand: |clip: MidiClip| { + SetColor { color: Option } => { + //(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 } => { + //(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 } => Ok(None), + SetCursor { cursor: usize } => Ok(None), +}); + +def_command!(MidiEditCommand: |editor: MidiEditor| { + Show { clip: Option>> } => { + 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 +}); + +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 } => { + 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 } => { + 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 } => todo!(), +}); + +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); + +def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), +}); + +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); + +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.samples.0[*index]); + sample + }); // TODO: undo + Ok(None) + }, + RecordCancel => { + sampler.recording = None; + Ok(None) + }, + PlaySample { slot: usize } => { + let slot = *slot; + if let Some(ref sample) = sampler.samples.0[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] Some(Self::Filter(f.expect("no filter"))))) +}); + +def_command!(SceneCommand: |scene: Scene| { + SetSize { size: usize } => { todo!() }, + SetZoom { size: usize } => { todo!() }, + SetName { name: Arc } => + swap_value(&mut scene.name, name, |name|Self::SetName{name}), + SetColor { color: ItemTheme } => + swap_value(&mut scene.color, color, |color|Self::SetColor{color}), +}); + +def_command!(TrackCommand: |track: Track| { + Stop => { track.sequencer.enqueue_next(None); Ok(None) }, + SetMute { mute: Option } => todo!(), + SetSolo { solo: Option } => todo!(), + SetSize { size: usize } => todo!(), + SetZoom { zoom: usize } => todo!(), + SetName { name: Arc } => + 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 } => + toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), + SetMon { mon: Option } => + toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), +}); + +pub(crate) use self::size::*; +mod size { + use crate::*; + /// Define a type alias for iterators of sized items (columns). + macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a> = + Iterator + Send + Sync + 'a; + } + } + def_sizes_iter!(InputsSizes => MidiInput); + def_sizes_iter!(OutputsSizes => MidiOutput); + def_sizes_iter!(PortsSizes => Arc, [Connect]); + def_sizes_iter!(ScenesSizes => Scene); + def_sizes_iter!(TracksSizes => Track); +} + +pub(crate) use self::view::*; +mod view { + use crate::*; + + pub fn view_logo () -> impl Content { + Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::Y(1, ""), + Fixed::Y(1, ""), + Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + }))) + } + + pub fn view_transport ( + play: bool, + bpm: Arc>, + beat: Arc>, + time: Arc>, + ) -> impl Content { + let theme = ItemTheme::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::XY(Align::w(button_play_pause(play))), + Fill::XY(Align::e(row!( + FieldH(theme, "BPM", bpm), + FieldH(theme, "Beat", beat), + FieldH(theme, "Time", time), + ))) + ))) + } + + pub fn view_status ( + sel: Option>, + sr: Arc>, + buf: Arc>, + lat: Arc>, + ) -> impl Content { + let theme = ItemTheme::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), + Fill::XY(Align::e(row!( + FieldH(theme, "SR", sr), + FieldH(theme, "Buf", buf), + FieldH(theme, "Lat", lat), + ))) + ))) + } + + pub fn button_play_pause (playing: bool) -> impl Content { + let compact = true;//self.is_editing(); + Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + Either::new(compact, + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + )), + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + )) + ) + ) + } + + #[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Content, + button_add: impl Content, + content: impl Content, + ) -> impl Content { + 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) -> impl Content { + 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 fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + '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 + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + Bsp::s(left, right) + } + + pub fn draw_info (sample: Option<&Arc>>) -> impl Content + 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)), + )) + })) + } + + pub fn draw_info_v (sample: Option<&Arc>>) -> impl Content + 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", + ))))) + } + + pub fn draw_status (sample: Option<&Arc>>) -> impl Content { + 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()))) + } + + pub fn view_track_header (theme: ItemTheme, content: impl Content) -> impl Content { + Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content)))) + } + + pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Content + 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 fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Content + 'a { + 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 fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Content + 'a { + per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) + } + + pub fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a + ) -> impl Content + 'a { + Map::new(iter, move|( + _index, name, connections, y, y2 + ): (usize, &'a Arc, &'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 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 }) + } + + pub fn draw_sample ( + to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool + ) -> Usually { + 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) + } +} diff --git a/app/tek_impls.rs b/app/tek_impls.rs index dd542cd8..ba12a595 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -1,6 +1,11 @@ use crate::*; +use std::fmt::Write; +use std::path::PathBuf; +use std::ffi::OsString; audio!(App: tek_jack_process, tek_jack_event); +audio!(Lv2: lv2_jack_process); +audio!(Sampler: sampler_jack_process); /// Command-line configuration. #[cfg(feature = "cli")] impl Cli { @@ -445,7 +450,7 @@ impl Binding { impl App { pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } /// Set modal dialog. @@ -714,3 +719,3737 @@ impl View for App { } + +impl Gettable for AtomicBool { + fn get (&self) -> bool { + self.load(Relaxed) + } +} + +impl InteriorMutable for AtomicBool { + fn set (&self, value: bool) -> bool { + self.swap(value, Relaxed) + } +} + +impl Gettable for AtomicUsize { + fn get (&self) -> usize { + self.load(Relaxed) + } +} + +impl InteriorMutable for AtomicUsize { + fn set (&self, value: usize) -> usize { + self.swap(value, Relaxed) + } +} + +//impl>> MaybeHas for U { + //fn get (&self) -> Option<&T> { + //Has::>::get(self).as_ref() + //} +//} + +impl Default for MidiCursor { + fn default () -> Self { + Self { + time_pos: Arc::new(0.into()), + note_pos: Arc::new(36.into()), + note_len: Arc::new(24.into()), + } + } +} + +impl NotePoint for MidiCursor { + fn note_len (&self) -> &AtomicUsize { + &self.note_len + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pos + } +} + +impl TimePoint for MidiCursor { + fn time_pos (&self) -> &AtomicUsize { + self.time_pos.as_ref() + } +} + +impl MidiPoint for T {} + +from!(MidiSelection: |data:(usize, bool)| Self { + time_len: Arc::new(0.into()), + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(data.0.into()), + time_lock: Arc::new(data.1.into()), +}); + +impl MidiRange for T {} + +impl TimeRange for MidiSelection { + fn time_len (&self) -> &AtomicUsize { &self.time_len } + fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } + fn time_lock (&self) -> &AtomicBool { &self.time_lock } + fn time_start (&self) -> &AtomicUsize { &self.time_start } + fn time_axis (&self) -> &AtomicUsize { &self.time_axis } +} + +impl NoteRange for MidiSelection { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.note_axis } +} + +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> Arc { + self.timebase.format_beats_1(self.pulse.get()).into() + } +} + +impl LaunchSync { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} + +impl Quantize { + pub fn next (&self) -> f64 { + note_duration_next(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + note_duration_prev(self.get() as usize) as f64 + } +} + +impl Iterator for TicksIterator { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} + +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { + TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4).into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { + let mut string = String::with_capacity(16); + self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); + string.into() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() + } +} + +impl Default for Timebase { + fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } +} +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} + +impl Microsecond { + #[inline] pub fn format_msu (&self) -> Arc { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}").into() + } +} + +/// Implement an arithmetic operation for a unit of time +#[macro_export] macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} + +/// Define and implement a unit of time +#[macro_export] macro_rules! impl_time_unit { + ($T:ident) => { + impl Gettable for $T { + fn get (&self) -> f64 { self.0.load(Relaxed) } + } + impl InteriorMutable for $T { + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Relaxed); + old + } + } + impl TimeUnit for $T {} + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } + impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } + impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } + impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } + } +} + +impl_time_unit!(SampleCount); +impl_time_unit!(SampleRate); +impl_time_unit!(Microsecond); +impl_time_unit!(Quantize); +impl_time_unit!(PulsesPerQuaver); +impl_time_unit!(Pulse); +impl_time_unit!(BeatsPerMinute); +impl_time_unit!(LaunchSync); + +/// Implement [Jack] constructor and methods +impl<'j> Jack<'j> { + + /// Register new [Client] and wrap it for shared use. + pub fn new_run + Audio + Send + Sync + 'static> ( + name: &impl AsRef, + init: impl FnOnce(Jack<'j>)->Usually + ) -> Usually>> { + Jack::new(name)?.run(init) + } + + pub fn new (name: &impl AsRef) -> Usually { + let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; + Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) + } + + pub fn run + Audio + Send + Sync + 'static> + (self, init: impl FnOnce(Self)->Usually) -> Usually>> + { + let client_state = self.0.clone(); + let app: Arc> = Arc::new(RwLock::new(init(self)?)); + let mut state = Activating; + std::mem::swap(&mut*client_state.write().unwrap(), &mut state); + if let Inactive(client) = state { + // This is the misc notifications handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [JackEvent], which is + // one of the available misc notifications. + let notify = JackNotify(Box::new({ + let app = app.clone(); + move|event|(&mut*app.write().unwrap()).handle(event) + }) as BoxedJackEventHandler); + // This is the main processing handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [Client] and [ProcessScope] + // and passes them down to the `app`'s `process` callback, which in turn + // implements audio and MIDI input and output on a realtime basis. + let process = ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &_, s: &_|if let Ok(mut app) = app.write() { + app.process(c, s) + } else { + Control::Quit + } + }) as BoxedAudioHandler); + // Launch a client with the two handlers. + *client_state.write().unwrap() = Active( + client.activate_async(notify, process)? + ); + } else { + unreachable!(); + } + Ok(app) + } + + /// Run something with the client. + pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + match &*self.0.read().unwrap() { + Inert => panic!("jack client not activated"), + Inactive(client) => op(client), + Activating => panic!("jack client has not finished activation"), + Active(client) => op(client.as_client()), + } + } +} + +impl<'j> HasJack<'j> for Jack<'j> { + fn jack (&self) -> &Jack<'j> { + self + } +} + +impl<'j> HasJack<'j> for &Jack<'j> { + fn jack (&self) -> &Jack<'j> { + self + } +} + +impl NotificationHandler for JackNotify { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} + +impl JackPerfModel for PerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store( + self.clock.delta_as_nanos(t0, t1) as f64, + Relaxed, + ); + self.window.store( + scope.cycle_times().unwrap().period_usecs as f64, + Relaxed, + ); + } + } +} + +has!(Jack<'static>: |self: Arrangement|self.jack); +has!(Measure: |self: Arrangement|self.size); + +#[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); +#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); +#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); +#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); +#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); + +#[cfg(feature = "track")] impl TracksView for Arrangement {} // -> to auto? + +#[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); +#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| + { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; + { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); + +#[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); +#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| + { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; + { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); + +#[cfg(feature = "select")] impl Arrangement { + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } + #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } + #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } + #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } + fn selected_device (&self) -> Option { + todo!() + } + fn unselect (&self) -> Selection { + Selection::Nothing + } +} + +#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} + +impl HasJack<'static> for Arrangement { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} + +impl Arrangement { + /// Create a new arrangement. + pub fn new ( + jack: &Jack<'static>, + name: Option>, + clock: Clock, + tracks: Vec, + scenes: Vec, + midi_ins: Vec, + midi_outs: Vec, + ) -> Self { + Self { + clock, tracks, scenes, midi_ins, midi_outs, + jack: jack.clone(), + name: name.unwrap_or_default(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + ..Default::default() + } + } + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } +} + + +#[cfg(feature = "track")] impl Arrangement { + /// Get the active track + pub fn get_track (&self) -> Option<&Track> { + let index = self.selection().track()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active track + pub fn get_track_mut (&mut self) -> Option<&mut Track> { + let index = self.selection().track()?; + Has::>::get_mut(self).get_mut(index) + } + /// Add multiple tracks + pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + /// Add a track + pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } + + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { + Bsp::s( + Fixed::Y(1, self.view_inputs_header()), + Thunk::new(|to: &mut TuiOut|{ + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) + } + }) + ) + } + + fn view_inputs_header (&self) -> impl Content + '_ { + Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + )))))) + }))) + } + + fn view_inputs_row (&self, port: &MidiInput) -> impl Content { + Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + ))))) + }))) + } + + pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + let list = Bsp::s( + Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::Y(1,Fill::X(Bsp::e( + Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + Fill::X(Align::e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Fill::X( + Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::X(track_width(index, track), + Thunk::new(|to: &mut TuiOut|{ + to.place(&Fixed::Y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, "play "), "play "), + Either::new(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::Y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, " ● "), " · "), + Either::new(false, Tui::fg(Yellow, " ● "), " · "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&Fixed::Y(1, Fill::X(""))); + } + }})))}})))))) + } + + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::XY(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, + |_, _index|Fixed::XY(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + Align::nw(format!(" · {}", "--"))))))))); + })) + } + +} + +#[cfg(feature = "scene")] impl Arrangement { + /// Get the active scene + pub fn get_scene (&self) -> Option<&Scene> { + let index = self.selection().scene()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active scene + pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { + let index = self.selection().scene()?; + Has::>::get_mut(self).get_mut(index) + } +} + +#[cfg(feature = "scene")] impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.measure_height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.measure_width() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} + +#[cfg(feature = "clip")] impl Arrangement { + /// Get the active clip + pub fn get_clip (&self) -> Option>> { + self.get_scene()?.clips.get(self.selection().track()?)?.clone() + } + /// Put a clip in a slot + pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + old + }) + } + /// Toggle looping for the active clip + pub fn toggle_loop (&mut self) { + if let Some(clip) = self.get_clip() { + clip.write().unwrap().toggle_loop() + } + } +} + +#[cfg(feature = "sampler")] impl Arrangement { + /// Get the first sampler of the active track + pub fn sampler (&self) -> Option<&Sampler> { + self.get_track()?.sampler(0) + } + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.get_track_mut()?.sampler_mut(0) + } +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.size_inner } +} + +impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + t => matches!(other, t) + } + } +} + +impl Browse { + + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { cwd, dirs, files, ..Default::default() }) + } + + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } + + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } + +} + +impl Browse { + fn _todo_stub_path_buf (&self) -> PathBuf { + todo!() + } + fn _todo_stub_usize (&self) -> usize { + todo!() + } + fn _todo_stub_arc_str (&self) -> Arc { + todo!() + } +} + + +impl HasContent for Browse { + fn content (&self) -> impl Content { + Map::south(1, ||EntriesIterator { + offset: 0, + index: 0, + length: self.dirs.len() + self.files.len(), + browser: self, + }, |entry, _index|Fill::X(Align::w(entry))) + } +} + +impl<'a> Iterator for EntriesIterator<'a> { + type Item = Modify<&'a str>; + fn next (&mut self) -> Option { + let dirs = self.browser.dirs.len(); + let files = self.browser.files.len(); + let index = self.index; + if self.index < dirs { + self.index += 1; + Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + } else if self.index < dirs + files { + self.index += 1; + Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + } else { + None + } + } +} + +impl MidiClip { + pub fn new ( + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().into(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemTheme::random) + } + } + pub fn count_midi_messages (&self) -> usize { + let mut count = 0; + for tick in self.notes.iter() { + count += tick.len(); + } + count + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend clip first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + false + } + pub fn stop_all () -> Self { + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) + ) + } +} + +impl PartialEq for MidiClip { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for MidiClip {} + +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} +//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + +impl> HasClock for T { + fn clock (&self) -> &Clock { self.get() } + fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } +} + +impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } +} + +impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &BeatsPerMinute { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &PulsesPerQuaver { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } +} + +impl Clock { + fn _todo_provide_u32 (&self) -> u32 { + todo!() + } + fn _todo_provide_opt_u32 (&self) -> Option { + todo!() + } + fn _todo_provide_f64 (&self) -> f64 { + todo!() + } +} + +impl Command for ClockCommand { + fn execute (&self, state: &mut T) -> Perhaps { + self.execute(state.clock_mut()) // awesome + } +} + +impl Default for ClockView { + fn default () -> Self { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } + } +} + +impl ClockView { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + + //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) + //-> Arc> + //{ + //let data = (track, tracks); + //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); + //cache.read().unwrap().trks.view.clone() + //} + + //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) + //-> impl Content + //{ + //let data = (scene, scenes); + //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); + //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) + //} + + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); + } + } + + //pub fn view_h2 (&self) -> impl Content { + //let cache = self.project.clock.view_cache.clone(); + //let cache = cache.read().unwrap(); + //add(&Fixed::x(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::x(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::x(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + //add(&Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( + //self.tracks(), + //self.scenes() + //))))), + //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) + //)); + ////if let Some(last) = self.history.last() { + ////add(&FieldV(theme, format!("History ({})", self.history.len()), + ////Fill::X(Align::w(format!("{:?}", last.0))))); + ////} + //} +} + + +impl>> HasEditor for T {} + +has!(Measure: |self: MidiEditor|self.size); + +impl Default for MidiEditor { + fn default () -> Self { + Self { + size: Measure::new(0, 0), + mode: PianoHorizontal::new(None), + } + } +} + +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} + +from!(MidiEditor: |clip: &Arc>| { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model +}); + +from!(MidiEditor: |clip: Option>>| { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); + +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos((note_end + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } + pub fn clip_status (&self) -> impl Content + '_ { + let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("f2", "name ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("l", "ength ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("r", "epeat ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Content + '_ { + let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", note_pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("t", "ime ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("z", "lock ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + Fill::X(Align::w(Bsp::e( + button_2("x", "note ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } +} + +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} + +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} + +impl NotePoint for MidiEditor { + fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} + +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} + +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +impl Draw for MidiEditor { + fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } +} + +impl Layout for MidiEditor { + fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } +} + +impl HasContent for MidiEditor { + fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } +} + + +has!(Measure:|self:PianoHorizontal|self.size); + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(0, 0); + let mut range = MidiSelection::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiCursor::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +impl Draw for PianoHorizontal { + fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } +} +impl Layout for PianoHorizontal { + fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } +} +impl HasContent for PianoHorizontal { + fn content (&self) -> impl Content { + Bsp::s( + Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), + ) + } +} + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.rgb); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Content { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut TuiOut|{ + let source = buffer.read().unwrap(); + let XYWH(x0, y0, w, _h) = to.area(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Content { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x0, y0, w, _) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Content { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x, y0, _w, _h) = to.area(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Content + '_ { + Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x, y, w, _h) = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} + +impl Default for OctaveVertical { + fn default () -> Self { + Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } + } +} +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} +impl HasContent for OctaveVertical { + fn content (&self) -> impl Content + '_ { + row!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} + +impl Lv2 { + + pub fn new ( + jack: &Jack<'static>, + name: &str, + uri: &str, + ) -> Usually { + let lv2_world = livi::World::with_load_bundle(&uri); + let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let lv2_plugin = lv2_world.iter_plugins().nth(0) + .unwrap_or_else(||panic!("plugin not found: {uri}")); + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(uri).into()), + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + lv2_instance: unsafe { + lv2_plugin + .instantiate(lv2_features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + lv2_port_list: lv2_plugin.ports().collect::>(), + lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + lv2_ui_thread: None, + lv2_world, + lv2_features, + lv2_plugin, + }) + } + + const INPUT_BUFFER: usize = 1024; + +} + + +fn lv2_jack_process ( + Lv2 { + midi_ins, midi_outs, audio_ins, audio_outs, + lv2_features, lv2_instance, lv2_input_buffer, .. + }: &mut Lv2, + _client: &Client, + scope: &ProcessScope +) -> Control { + + let urid = lv2_features.midi_urid(); + + lv2_input_buffer.clear(); + + for port in midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &lv2_features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + lv2_input_buffer.push(atom); + } + + let mut outputs = vec![]; + + for _ in midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + lv2_features, + scope.n_frames() as usize + )); + } + + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(lv2_input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + + unsafe { + lv2_instance.run(scope.n_frames() as usize, ports).unwrap() + }; + + Control::Continue +} + +impl Draw for Lv2 { + fn draw (&self, to: &mut TuiOut) { + let area = to.area(); + let XYWH(x, y, _, height) = area; + let mut width = 20u16; + let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = self.lv2_port_list.get(i) { + let value = if let Some(value) = self.lv2_instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + let style = if i == self.selected { + Some(Style::default().green()) + } else { + None + } ; + to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); + } else { + break + } + } + draw_header(self, to, x, y, width); + } +} + +#[cfg(feature = "lv2_gui")] +impl LV2PluginUI { + pub fn new () -> Usually { + Ok(Self { window: None }) + } +} + +#[cfg(feature = "lv2_gui")] +impl ApplicationHandler for LV2PluginUI { + fn resumed (&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + self.window.as_ref().unwrap().set_visible(false); + event_loop.exit(); + }, + WindowEvent::RedrawRequested => { + self.window.as_ref().unwrap().request_redraw(); + } + _ => (), + } + } +} +impl Layout for RmsMeter {} +impl Draw for RmsMeter { + fn draw (&self, to: &mut TuiOut) { + let XYWH(x, y, w, h) = to.area(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); + } + } + } +} +impl Layout for Log10Meter {} +impl Draw for Log10Meter { + fn draw (&self, to: &mut TuiOut) { + let XYWH(x, y, w, h) = to.area(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"▌", x, y2 - y, Some(Style::default().green())); + } + } + } +} +impl Default for Pool { + fn default () -> Self { + //use PoolMode::*; + Self { + clip: 0.into(), + mode: None, + visible: true, + #[cfg(feature = "clip")] clips: Arc::from(RwLock::from(vec![])), + #[cfg(feature = "sampler")] samples: Arc::from(RwLock::from(vec![])), + #[cfg(feature = "browse")] browse: None, + } + } +} +impl Pool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} +#[macro_export] macro_rules! has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} +has_clips!(|self: Pool|self.clips); +has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); +from!(Pool: |clip:&Arc>|{ + let model = Self::default(); + model.clips.write().unwrap().push(clip.clone()); + model.clip.store(1, Relaxed); + model +}); +impl Pool { + fn _todo_usize_ (&self) -> usize { todo!() } + fn _todo_bool_ (&self) -> bool { todo!() } + fn _todo_clip_ (&self) -> MidiClip { todo!() } + fn _todo_path_ (&self) -> PathBuf { todo!() } + fn _todo_color_ (&self) -> ItemColor { todo!() } + fn _todo_str_ (&self) -> Arc { todo!() } + fn clip_new (&self) -> MidiClip { self.new_clip() } + fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } + fn clip_index_current (&self) -> usize { 0 } + fn clip_index_after (&self) -> usize { 0 } + fn clip_index_previous (&self) -> usize { 0 } + fn clip_index_next (&self) -> usize { 0 } + fn color_random (&self) -> ItemColor { ItemColor::random() } +} +impl<'a> HasContent for PoolView<'a> { + fn content (&self) -> impl Content { + let Self(pool) = self; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); + //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + //let height = pool.clips.read().unwrap().len() as u16; + Fixed::X(20, Fill::Y(Align::n(Map::new( + ||pool.clips().clone().into_iter(), + move|clip: Arc>, i: usize|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == pool.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; + Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( + Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), + Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), + Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + )))) + })))) + } +} +impl HasContent for ClipLength { + fn content (&self) -> impl Content + '_ { + use ClipLengthFocus::*; + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => row!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), + } + } +} + +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} +//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); +//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } +} + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for MidiInput { + type Port = MidiIn; + type Pair = MidiOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} + +impl>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { + self.get() + } + fn midi_ins_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + + +impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } +} + +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for MidiOutput { + type Port = MidiOut; + type Pair = MidiIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} + + +impl>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { + self.get() + } + fn midi_outs_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +/// Trail for thing that may gain new MIDI ports. +impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } +} + +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for AudioInput { + type Port = AudioIn; + type Pair = AudioOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for AudioOutput { + type Port = AudioOut; + type Pair = AudioIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl Draw for AddSampleModal { + fn draw (&self, _to: &mut TuiOut) { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; + //for (i, (is_dir, name)) in self.subdirs.iter() + //.map(|path|(true, path)) + //.chain(self.files.iter().map(|path|(false, path))) + //.enumerate() + //.skip(self.offset) + //{ + //if i >= area.h() as usize - 4 { + //break + //} + //let t = if is_dir { "" } else { "" }; + //let line = format!("{t} {}", name.to_string_lossy()); + //let line = &line[..line.len().min(area.w() as usize - 4)]; + //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { + //Style::default().green() + //} else { + //Style::default().white() + //}))?; + //} + //Lozenge(Style::default()).draw(to) + } +} + +impl Sampler { + + pub fn view_grid (&self) -> impl Content + use<'_> { + //let cells_x = 8u16; + //let cells_y = 8u16; + //let cell_width = 10u16; + //let cell_height = 2u16; + //let width = cells_x * cell_width; + //let height = cells_y * cell_height; + //let cols = Map::east( + //cell_width, + //move||0..cells_x, + //move|x, _|Map::south( + //cell_height, + //move||0..cells_y, + //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) + //) + //); + //cols + //Thunk::new(|to: &mut TuiOut|{ + //}) + "TODO" + } + + pub fn view_grid_cell <'a> ( + &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 + ) -> impl Content + use<'a> { + let cursor = self.cursor(); + let hi_fg = Color::Rgb(64, 64, 64); + let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; + let tx_fg = if let Some((index, _)) = self.recording + && index % 8 == x as usize + && index / 8 == y as usize + { + Color::Rgb(255, 64, 0) + } else { + Color::Rgb(255, 255, 255) + }; + let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { + Color::Rgb(96, 96, 96) + } else { + Color::Rgb(64, 64, 64) + }; + let lo_fg = Color::Rgb(64, 64, 64); + let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; + Fixed::XY(w, h, Bsp::s( + Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, Repeat::X(Phat::<()>::LO))), + Bsp::n( + Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, Repeat::X(Phat::<()>::HI))), + Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), + ), + )) + } + + const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; + + pub fn view_list <'a, T: NotePoint + NoteRange> ( + &'a self, compact: bool, editor: &T + ) -> impl Content + 'a { + let note_lo = editor.get_note_lo(); + let note_pt = editor.get_note_pos(); + let note_hi = editor.get_note_hi(); + Fixed::X(if compact { 4 } else { 12 }, Map::south( + 1, + move||(note_lo..=note_hi).rev(), + move|note, _index| { + //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); + let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; + let mut fg = Tui::g(160); + if let Some(mapped) = self.samples.get(note) { + let sample = mapped.read().unwrap(); + fg = if note == note_pt { + sample.color.lightest.rgb + } else { + Tui::g(224) + }; + bg = if note == note_pt { + sample.color.light.rgb + } else { + sample.color.base.rgb + }; + } + if let Some((index, _)) = self.recording { + if note == index { + bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; + fg = Color::Rgb(224,64,32) + } + } + Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) + })) + } + + pub fn view_list_item (&self, note: usize, compact: bool) -> String { + if compact { + String::default() + } else { + draw_list_item(&self.samples.get(note)) + } + } + + pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.samples.get(note_pt) { + Some(sample) + } else { + None + }))) + } + + pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { + Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.samples.get(note_pt) { + Some(sample) + } else { + None + }))) + } + + pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { + Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.samples.get(note_pt) { + Some(sample) + } else { + None + })) + } + + pub fn view_status (&self, index: usize) -> impl Content { + draw_status(self.samples.get(index).as_ref()) + } + + pub fn view_meters_input (&self) -> impl Content + use<'_> { + draw_meters(&self.input_meters) + } + + pub fn view_meters_output (&self) -> impl Content + use<'_> { + draw_meters(&self.output_meters) + } +} + +fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { + Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ + Fill::Y(RmsMeter(*value)) + }))) +} + +fn draw_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.read().unwrap(); + format!("{:8}", sample.name) + //format!("{:8} {:3} {:6}-{:6}/{:6}", + //sample.name, + //sample.gain, + //sample.start, + //sample.end, + //sample.channels[0].len() + //) + } else { + String::from("........") + } +} + +fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { + let min_db = -64.0; + Thunk::new(move|to: &mut TuiOut|{ + let XYWH(x, y, width, height) = to.area(); + let area = Rect { x, y, width, height }; + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }).render(area, &mut to.buffer); + } else { + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|_ctx| { + //let text = "press record to begin sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, &mut to.buffer); + } + }) +} + +impl Sampler { + fn sample_selected (&self) -> usize { + (self.get_note_pos() as u8).into() + } + fn sample_selected_pitch (&self) -> u7 { + (self.get_note_pos() as u8).into() + } +} + +impl AddSampleModal { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } +} + +impl AddSampleModal { + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +impl Sampler { + + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); + } else { + self.update_input_meters(scope); + } + } + + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + *meter = to_rms(slice); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + panic!("tried to record into the void") + } + } + + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + *meter = to_rms(slice); + } + } + + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; + } + } + + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; + let channel_count = buffer.len(); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } + } + + /// Write output buffer to output ports. + fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } + +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} + +impl Sampler { + pub fn new ( + jack: &Jack<'static>, + name: impl AsRef, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] audio_from: &[&[Connect];2], + #[cfg(feature = "port")] audio_to: &[&[Connect];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + output_gain: 1., + buffer: vec![vec![0.0;16384];2], + #[cfg(feature = "port")] midi_in: Some( + MidiInput::new(jack, &format!("M/{name}"), midi_from)? + ), + #[cfg(feature = "port")] audio_ins: vec![ + AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, + AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + #[cfg(feature = "port")] audio_outs: vec![ + AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, + AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + ..Default::default() + }) + } + /// Value of cursor + pub fn cursor (&self) -> (usize, usize) { + (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) + } +} + +impl NoteRange for Sampler { + fn note_lo (&self) -> &AtomicUsize { + &self.note_lo + } + fn note_axis (&self) -> &AtomicUsize { + &self.size.y + } +} + +impl NotePoint for Sampler { + fn note_len (&self) -> &AtomicUsize { + unreachable!(); + } + fn get_note_len (&self) -> usize { + 0 + } + fn set_note_len (&self, x: usize) -> usize { + 0 /*TODO?*/ + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pt + } + fn get_note_pos (&self) -> usize { + self.note_pt.load(Relaxed) + } + fn set_note_pos (&self, x: usize) -> usize { + let old = self.note_pt.swap(x, Relaxed); + self.cursor.0.store(x % 8, Relaxed); + self.cursor.1.store(x / 8, Relaxed); + old + } +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } +} + +impl Sample { + + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } + +} + +fn sampler_jack_process ( + state: &mut Sampler, _: &Client, scope: &ProcessScope +) -> Control { + if let Some(midi_in) = &state.midi_in { + for midi in midi_in.port().iter(scope) { + sampler_midi_in(&state.samples, &state.voices, midi) + } + } + state.process_audio_out(scope); + state.process_audio_in(scope); + Control::Continue +} + +/// Create [Voice]s from [Sample]s in response to MIDI input. +fn sampler_midi_in ( + samples: &Samples<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi +) { + if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(sample) = samples.get(key.as_int() as usize) { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller: _, value: _ } => { + // TODO + } + _ => {} + } + } +} + +impl Sample { + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } +} + + +impl> + Send + Sync> HasScenes for T {} +impl HasSceneScroll for Arrangement { + fn scene_scroll (&self) -> usize { self.scene_scroll } +} + +impl AddScene for T {} + + +impl Scene { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +impl> + Send + Sync> HasScene for T {} + + +impl Scene { + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} + +impl> HasSelection for T {} + +#[cfg(feature = "track")] +impl Selection { + pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } +} + +#[cfg(feature = "scene")] +impl Selection { + pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } +} + +impl Selection { + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match s.clip(*track) { + Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), + None => format!("T{track} S{scene}: Empty") + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } +} + +impl> HasSequencer for T { + fn sequencer (&self) -> &Sequencer { + self.get() + } + fn sequencer_mut (&mut self) -> &mut Sequencer { + self.get_mut() + } +} + +impl Default for Sequencer { + fn default () -> Self { + Self { + #[cfg(feature = "clock")] clock: Clock::default(), + #[cfg(feature = "clip")] play_clip: None, + #[cfg(feature = "clip")] next_clip: None, + #[cfg(feature = "port")] midi_ins: vec![], + #[cfg(feature = "port")] midi_outs: vec![], + + recording: false, + monitoring: true, + overdub: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + midi_buf: vec![], + reset: true, + } + } +} + +impl Sequencer { + pub fn new ( + name: impl AsRef, + jack: &Jack<'static>, + #[cfg(feature = "clock")] clock: Option<&Clock>, + #[cfg(feature = "clip")] clip: Option<&Arc>>, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] midi_to: &[Connect], + ) -> Usually { + let _name = name.as_ref(); + #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); + Ok(Self { + reset: true, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], + #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], + #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), + #[cfg(feature = "clock")] clock, + ..Default::default() + }) + } +} + +impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } +} + +#[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); +#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); +#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); + +impl MidiMonitor for Sequencer { + fn notes_in (&self) -> &Arc> { + &self.notes_in + } + fn monitoring (&self) -> bool { + self.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self.monitoring + } +} + +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } + fn overdub (&self) -> bool { + self.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self.overdub + } +} + +#[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} + +/// JACK process callback for a sequencer's clip sequencer/recorder. +impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } +} + +impl Sequencer { + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue + } + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue + } + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + let monitoring = self.monitoring(); + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + self.midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.write().unwrap(), &message); + } + } + } + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { + if self.monitoring() { + self.monitor(scope); + } + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); + } + if let Some((_start_at, _clip)) = &self.next_clip() { + self.record_next(); + } + } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} + +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { + &mut self.note_buf + } + fn midi_buf_mut (&mut self) -> &mut Vec>> { + &mut self.midi_buf + } +} + +impl> + Send + Sync> HasTracks for T {} + +impl HasTrackScroll for Arrangement { + fn track_scroll (&self) -> usize { + self.track_scroll + } +} + +impl> HasTrack for T { + fn track (&self) -> Option<&Track> { + self.get() + } + fn track_mut (&mut self) -> Option<&mut Track> { + self.get_mut() + } +} + +has!(Clock: |self: Track|self.sequencer.clock); +has!(Sequencer: |self: Track|self.sequencer); +impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, + ..Default::default() + }) + } + pub fn audio_ins (&self) -> &[AudioInput] { + self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() + } + pub fn audio_outs (&self) -> &[AudioOutput] { + self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { self.width += 1; } + fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } +} + +#[cfg(feature = "sampler")] +impl Track { + /// Create a new track connecting the [Sequencer] to a [Sampler]. + pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + audio_from: &[&[Connect];2], + audio_to: &[&[Connect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; + track.devices.push(Device::Sampler(Sampler::new( + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to + )?)); + Ok(track) + } + pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } +} + +impl ClipsView for T {} + +impl Track { + pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Content + 'a { + Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}) + } +} + + +impl Samples { + fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} + +impl Default for Samples { + fn default () -> Self { + Self([const { None }; N]) + } +} diff --git a/app/tek_struct.rs b/app/tek_struct.rs index 29f475e1..0f6e8c15 100644 --- a/app/tek_struct.rs +++ b/app/tek_struct.rs @@ -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>> +); + +/// 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), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(Arc, bool), + PortRegistration(PortId, bool), + PortRename(PortId, Arc, Arc), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Generic notification handler that emits [JackEvent] +/// +/// ``` +/// let notify = tek::JackNotify(|_|{}); +/// ``` +pub struct JackNotify(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> = Default::default(); +/// let mode = tek::Mode::>::default(); /// ``` #[derive(Default, Debug)] pub struct Mode { 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( /// 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), 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, + /// Note coordinate of cursor + pub note_pos: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +/// +/// ``` +/// +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiSelection { + pub time_len: Arc, + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +/// +/// ``` +/// +/// ``` +#[derive(Debug, Default, Clone)] pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} + +/// +/// ``` +/// +/// ``` +#[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>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +/// Contains memoized renders of clock values. +/// +/// Performance optimization. +#[derive(Debug)] pub struct ClockView { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +/// Arranger. +/// +/// ``` +/// let arranger = tek::Arrangement::default(); +/// ``` +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// 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>, + /// Display size + pub size: Measure, + /// Display size of clips area + pub size_inner: Measure, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// 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, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub 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, +} + +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>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + +/// 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, + /// 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, + /// 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>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// 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, + pub path: Option>, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, + + pub lv2_world: livi::World, + pub lv2_instance: livi::Instance, + pub lv2_plugin: livi::Plugin, + pub lv2_features: Arc, + pub lv2_port_list: Vec, + pub lv2_input_buffer: Vec, + pub lv2_ui_thread: Option>, +} + +/// A LV2 plugin's X11 UI. +#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { + pub window: Option +} + +#[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, + /// Embedded file browse + #[cfg(feature = "browse")] pub browse: Option, + /// Collection of MIDI clips. + #[cfg(feature = "clip")] pub clips: Arc>>>>, + /// Collection of sound samples. + #[cfg(feature = "sampler")] pub samples: Arc>>>>, +} + +/// 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, +} + +/// 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, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// 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, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// 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, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// 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, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +/// Plays [Voice]s from [Sample]s. +/// +/// ``` +/// let sampler = tek::Sampler::default(); +/// ``` +#[derive(Debug, Default)] pub struct Sampler { + /// Name of sampler. + pub name: Arc, + /// Device color. + pub color: ItemTheme, + /// Audio input ports. Samples get recorded here. + #[cfg(feature = "port")] pub audio_ins: Vec, + /// Audio input meters. + #[cfg(feature = "meter")] pub input_meters: Vec, + /// Sample currently being recorded. + pub recording: Option<(usize, Option>>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub samples: Samples<128>, + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// MIDI input port. Triggers sample playback. + #[cfg(feature = "port")] pub midi_in: Option, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Audio output ports. Voices get played here. + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Audio output meters. + #[cfg(feature = "meter")] pub output_meters: Vec, + /// 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, + /// Size of rendered sampler. + pub size: Measure, + /// 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( + pub [Option>>;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, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +pub struct AddSampleModal { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +/// 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>>)>, + /// Start time and next clip + #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, + /// Record from MIDI ports to current sequence. + #[cfg(feature = "port")] pub midi_ins: Vec, + /// Play from current sequence to MIDI ports + #[cfg(feature = "port")] pub midi_outs: Vec, + /// 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>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, +} + +/// 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, + /// 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, +} + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + +/// Modes for clip pool +#[derive(Debug, Clone)] pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// 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), + /** Match regular expression */ + RegExp(Arc), +} + +#[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), +} diff --git a/device/device_trait.rs b/app/tek_trait.rs similarity index 79% rename from device/device_trait.rs rename to app/tek_trait.rs index 9e1aa63a..6b21e441 100644 --- a/device/device_trait.rs +++ b/app/tek_trait.rs @@ -1,4 +1,246 @@ use crate::*; +use std::sync::atomic::Ordering; + +/// Things that can provide a [jack::Client] reference. +/// +/// ``` +/// use tek::{Jack, HasJack}; +/// +/// let jack: &Jack = Jacked::default().jack(); +/// +/// #[derive(Default)] struct Jacked<'j>(Jack<'j>); +/// +/// impl<'j> tek::HasJack<'j> for Jacked<'j> { +/// fn jack (&self) -> &Jack<'j> { &self.0 } +/// } +/// ``` +pub trait HasJack<'j>: Send + Sync { + + /// Return the internal [jack::Client] handle + /// that lets you call the JACK API. + fn jack (&self) -> &Jack<'j>; + + fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + self.jack().with_client(op) + } + + fn port_by_name (&self, name: &str) -> Option> { + self.with_client(|client|client.port_by_name(name)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn register_port (&self, name: impl AsRef) -> Usually> { + self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) + } + + fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> { + if enable { + self.with_client(|client|match client.register_timebase_callback(false, callback) { + Ok(_) => Ok(()), + Err(e) => Err(e) + })? + } + Ok(()) + } + + fn sync_follow (&self, _enable: bool) -> Usually<()> { + // TODO: sync follow + Ok(()) + } +} + +/// Trait for thing that has a JACK process callback. +pub trait Audio { + + /// Handle a JACK event. + fn handle (&mut self, _event: JackEvent) {} + + /// Projecss a JACK chunk. + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + + /// The JACK process callback function passed to the server. + fn callback ( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } + +} + +/// Implement [Audio]: provide JACK callbacks. +#[macro_export] macro_rules! audio { + + (| + $self1:ident: + $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident + |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { + #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } + $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? + } + }; + + ($Struct:ident: $process:ident, $handle:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + #[inline] fn handle (&mut self, e: JackEvent) { + $handle(self, e) + } + } + }; + + ($Struct:ident: $process:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + } + }; + +} + +pub trait JackPerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); +} + + +//pub trait MaybeHas: Send + Sync { + //fn get (&self) -> Option<&T>; +//} + +pub trait HasN: Send + Sync { + fn get_nth (&self, key: usize) -> &T; + fn get_nth_mut (&mut self, key: usize) -> &mut T; +} + +pub trait Gettable { + /// Returns current value + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; +} + +pub trait InteriorMutable: Gettable { + /// Sets new value, returns old + fn set (&self, value: T) -> T; +} + +pub trait NotePoint { + fn note_len (&self) -> &AtomicUsize; + /// Get the current length of the note cursor. + fn get_note_len (&self) -> usize { + self.note_len().load(Relaxed) + } + /// Set the length of the note cursor, returning the previous value. + fn set_note_len (&self, x: usize) -> usize { + self.note_len().swap(x, Relaxed) + } + + fn note_pos (&self) -> &AtomicUsize; + /// Get the current pitch of the note cursor. + fn get_note_pos (&self) -> usize { + self.note_pos().load(Relaxed).min(127) + } + /// Set the current pitch fo the note cursor, returning the previous value. + fn set_note_pos (&self, x: usize) -> usize { + self.note_pos().swap(x.min(127), Relaxed) + } +} + +pub trait TimePoint { + fn time_pos (&self) -> &AtomicUsize; + /// Get the current time position of the note cursor. + fn get_time_pos (&self) -> usize { + self.time_pos().load(Relaxed) + } + /// Set the current time position of the note cursor, returning the previous value. + fn set_time_pos (&self, x: usize) -> usize { + self.time_pos().swap(x, Relaxed) + } +} + +pub trait MidiPoint: NotePoint + TimePoint { + /// Get the current end of the note cursor. + fn get_note_end (&self) -> usize { + self.get_time_pos() + self.get_note_len() + } +} + +pub trait TimeRange { + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Ordering::Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Ordering::Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Ordering::Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Ordering::Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Ordering::Relaxed) + } + fn time_start (&self) -> &AtomicUsize; + fn get_time_start (&self) -> usize { + self.time_start().load(Ordering::Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Ordering::Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Ordering::Relaxed) + } + fn get_time_end (&self) -> usize { + self.time_start().get() + self.time_axis().get() * self.time_zoom().get() + } +} + +pub trait NoteRange { + fn note_lo (&self) -> &AtomicUsize; + fn get_note_lo (&self) -> usize { + self.note_lo().load(Ordering::Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Ordering::Relaxed) + } + fn note_axis (&self) -> &AtomicUsize; + fn get_note_axis (&self) -> usize { + self.note_axis().load(Ordering::Relaxed) + } + fn get_note_hi (&self) -> usize { + (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) + } +} + +pub trait MidiRange: TimeRange + NoteRange {} + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit: InteriorMutable {} pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; @@ -75,8 +317,9 @@ pub trait HasScenes: Has> + Send + Sync { } /// ``` +/// use tek::{MidiEditor, HasEditor, tengri::Has}; /// struct TestEditorHost(Option); -/// has!(Option: |self: TestEditorHost|self.0); +/// tek::tengri::has!(Option: |self: TestEditorHost|self.0); /// let mut host = TestEditorHost(Some(MidiEditor::default())); /// let _ = host.editor(); /// let _ = host.editor_mut(); @@ -715,12 +958,15 @@ pub trait JackPort: HasJack<'static> { } 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; + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; } Ok(()) } @@ -745,7 +991,7 @@ pub trait JackPort: HasJack<'static> { }) } fn connect_regexp <'k> ( - &'k self, re: &str, scope: ConnectScope + &'k self, re: &str, scope: Option ) -> Usually, Arc, ConnectStatus)>> { self.with_client(move|c|{ let mut status = vec![]; @@ -755,7 +1001,7 @@ pub trait JackPort: HasJack<'static> { 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 { + if port_status == Connected && scope == Some(One) { break } } diff --git a/engine/jack_types.rs b/app/tek_type.rs similarity index 57% rename from engine/jack_types.rs rename to app/tek_type.rs index a64cd1d9..bcc90fb8 100644 --- a/engine/jack_types.rs +++ b/app/tek_type.rs @@ -25,3 +25,27 @@ pub type DynamicNotifications<'j> = /// Boxed [JackEvent] callback. pub type BoxedJackEventHandler<'j> = Box; + +pub type MidiData = + Vec>; + +pub type ClipPool = + Vec>>; + +pub type CollectedMidiInput<'a> = + Vec, MidiError>)>>; + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +pub type MidiSample = + (Option, Arc>); + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +/// Collection of view definitions. +pub type Views = Arc, Arc>>>; diff --git a/device/.scratch.rs b/device/.scratch.rs deleted file mode 100644 index f5bf3fbd..00000000 --- a/device/.scratch.rs +++ /dev/null @@ -1,614 +0,0 @@ - //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) => { - //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 } => { - //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 { - //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 { - //todo!() - //} - //fn file_browser_path (&self) -> PathBuf { - //todo!(); - //} - ///// Immutable reference to sample at cursor. - //fn sample_selected (&self) -> Option>> { - //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::Select(state.set_note_pos(i)) - //} - ///// Assign sample to slot - //set (&self, slot: u7, sample: Option>>) -> Option { - //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 { - //todo!() - //} - //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { - //todo!() - //} - //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { - //todo!() - //} - //note_off (&self, state: &mut Sampler, slot: u7) -> Option { - //todo!() - //} - //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { - //Some(Self::SetSample(p, state.set_sample(p, s))) - //} - //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { - //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>>] 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>>] - ////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, &'a [Connect], usize, usize)> -//) -> impl Content + '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())); diff --git a/device/Cargo.toml b/device/Cargo.toml deleted file mode 100644 index 345640b0..00000000 --- a/device/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "tek_device" -edition = { workspace = true } -version = { workspace = true } - -[lib] -path = "device.rs" - -[target.'cfg(target_os = "linux")'] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[dependencies] -tek_engine = { path = "../engine" } - -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 } - -[dev-dependencies] -proptest = { workspace = true } -proptest-derive = { workspace = true } - -[features] -arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"] -browse = [] -clap = [] -cli = ["dep:clap"] -clip = [] -clock = [] -default = ["cli", "arranger", "sampler", "track", "lv2"] -editor = [] -host = ["lv2"] -lv2 = ["port", "livi"] -lv2_gui = ["lv2", "winit"] -meter = [] -mixer = [] -pool = [] -port = [] -sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] -select = [] -scene = [] -sequencer = ["port", "clock", "uuid", "pool"] -sf2 = [] -track = [] -vst2 = [] -vst3 = [] diff --git a/device/device.rs b/device/device.rs deleted file mode 100644 index d2a95464..00000000 --- a/device/device.rs +++ /dev/null @@ -1,820 +0,0 @@ -#![feature(trait_alias)] - -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 ::{ - tek_engine::*, - tek_engine::tengri::{ - Usually, Perhaps, Has, MaybeHas, has, maybe_has, from, - input::*, - output::*, - tui::*, - tui::ratatui, - tui::ratatui::widgets::{Widget, canvas::{Canvas, Line}}, - tui::ratatui::prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - }, - std::{ - cmp::Ord, - ffi::OsString, - fmt::{Debug, Formatter}, - fs::File, - path::PathBuf, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - } -}; - -#[cfg(feature = "sampler")] pub(crate) use symphonia::{ - core::{ - formats::Packet, - codecs::{Decoder, CODEC_TYPE_NULL}, - //errors::Error as SymphoniaError, - io::MediaSourceStream, - probe::Hint, - audio::SampleBuffer, - }, - default::get_codecs, -}; - -#[cfg(feature = "lv2")] use std::thread::{spawn, JoinHandle}; - -#[cfg(feature = "lv2_gui")] use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a> = - Iterator + Send + Sync + 'a; - } -} - - -pub fn view_transport ( - play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, -) -> impl Content { - let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::XY(Align::w(button_play_pause(play))), - Fill::XY(Align::e(row!( - FieldH(theme, "BPM", bpm), - FieldH(theme, "Beat", beat), - FieldH(theme, "Time", time), - ))) - ))) -} - -pub fn view_status ( - sel: Option>, - sr: Arc>, - buf: Arc>, - lat: Arc>, -) -> impl Content { - let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), - Fill::XY(Align::e(row!( - FieldH(theme, "SR", sr), - FieldH(theme, "Buf", buf), - FieldH(theme, "Lat", lat), - ))) - ))) -} - -pub fn button_play_pause (playing: bool) -> impl Content { - let compact = true;//self.is_editing(); - Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - Either::new(compact, - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - )), - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) - )) - ) - ) -} - -pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U -) -> Perhaps { - if *target == *value { - Ok(None) - } else { - let mut value = value.clone(); - std::mem::swap(target, &mut value); - Ok(Some(returned(value))) - } -} - -pub fn toggle_bool ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U -) -> Perhaps { - let mut value = value.unwrap_or(!*target); - if value == *target { - Ok(None) - } else { - std::mem::swap(target, &mut value); - Ok(Some(returned(Some(value)))) - } -} - -pub fn device_kinds () -> &'static [&'static str] { - &[ - #[cfg(feature = "sampler")] "Sampler", - #[cfg(feature = "lv2")] "Plugin (LV2)", - ] -} - -impl>> HasDevices for T { - fn devices (&self) -> &Vec { - self.get() - } - fn devices_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } - pub fn midi_ins (&self) -> &[MidiInput] { - match self { - //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], - _ => todo!() - } - } - pub fn midi_outs (&self) -> &[MidiOutput] { - match self { - Self::Sampler(_) => &[], - _ => todo!() - } - } - pub fn audio_ins (&self) -> &[AudioInput] { - match self { - Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), - _ => todo!() - } - } - pub fn audio_outs (&self) -> &[AudioOutput] { - match self { - Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), - _ => todo!() - } - } -} - -audio!(|self: DeviceAudio<'a>, client, scope|{ - use Device::*; - match self.0 { - Mute => { Control::Continue }, - Bypass => { /*TODO*/ Control::Continue }, - #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), - #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO - } -}); - -def_command!(ClockCommand: |clock: Clock| { - SeekUsec { usec: f64 } => { - clock.playhead.update_from_usec(*usec); Ok(None) }, - SeekSample { sample: f64 } => { - clock.playhead.update_from_sample(*sample); Ok(None) }, - SeekPulse { pulse: f64 } => { - clock.playhead.update_from_pulse(*pulse); Ok(None) }, - SetBpm { bpm: f64 } => Ok(Some( - Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), - SetQuant { quant: f64 } => Ok(Some( - Self::SetQuant { quant: clock.quant.set(*quant) })), - SetSync { sync: f64 } => Ok(Some( - Self::SetSync { sync: clock.sync.set(*sync) })), - - Play { position: Option } => { - clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, - Pause { position: Option } => { - clock.pause_at(*position)?; Ok(None) }, - - TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { - clock.pause_at(Some(*position))?; None - } else { - clock.play_from(Some(*position))?; None - }), -}); -def_command!(DeviceCommand: |device: Device| {}); -def_command!(ClipCommand: |clip: MidiClip| { - SetColor { color: Option } => { - //(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 } => { - //(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 } => Ok(None), - SetCursor { cursor: usize } => Ok(None), -}); -//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() - //.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, - button_add: impl Content, - content: impl Content, -) -> impl Content { - 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) -> impl Content { - 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>> { $cb } - } - } -} -def_command!(MidiEditCommand: |editor: MidiEditor| { - Show { clip: Option>> } => { - 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 -{ - (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> { - 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 ( - buffer: &mut [Vec], 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 ( - buffer: &mut [Vec], 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 + '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 + 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 } => { - 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 } => { - 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 } => todo!(), -}); -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); -def_sizes_iter!(InputsSizes => MidiInput); -def_sizes_iter!(OutputsSizes => MidiOutput); -def_sizes_iter!(PortsSizes => Arc, [Connect]); -def_sizes_iter!(ScenesSizes => Scene); - -fn draw_info (sample: Option<&Arc>>) -> impl Content + 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>>) -> impl Content + 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>>) -> impl Content { - 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 { - 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>)> { - todo!(); -} - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - 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] Some(Self::Filter(f.expect("no filter"))))) -}); -def_command!(SceneCommand: |scene: Scene| { - SetSize { size: usize } => { todo!() }, - SetZoom { size: usize } => { todo!() }, - SetName { name: Arc } => - 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 } => todo!(), - SetSolo { solo: Option } => todo!(), - SetSize { size: usize } => todo!(), - SetZoom { zoom: usize } => todo!(), - SetName { name: Arc } => - 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 } => - toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), - SetMon { mon: Option } => - 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) -> impl Content { - 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 + 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 + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + '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 + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + '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 + 'a { - Map::new(iter, move|( - _index, name, connections, y, y2 - ): (usize, &'a Arc, &'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 ( - host: &Arc>>, _path: &str -) -> Usually { - 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()? - }) -} diff --git a/device/device_impl.rs b/device/device_impl.rs deleted file mode 100644 index 6005f533..00000000 --- a/device/device_impl.rs +++ /dev/null @@ -1,3233 +0,0 @@ -use crate::*; -use std::fmt::Write; -use std::path::PathBuf; -use std::ffi::OsString; - -has!(Jack<'static>: |self: Arrangement|self.jack); -has!(Measure: |self: Arrangement|self.size); - -#[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); -#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); -#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); -#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); - -#[cfg(feature = "track")] impl TracksView for Arrangement {} // -> to auto? - -#[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); -#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); - -#[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); -#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); - -#[cfg(feature = "select")] impl Arrangement { - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } - #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } - #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } - #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } - fn selected_device (&self) -> Option { - todo!() - } - fn unselect (&self) -> Selection { - Selection::Nothing - } -} - -#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} - -impl HasJack<'static> for Arrangement { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} - -impl Arrangement { - /// Create a new arrangement. - pub fn new ( - jack: &Jack<'static>, - name: Option>, - clock: Clock, - tracks: Vec, - scenes: Vec, - midi_ins: Vec, - midi_outs: Vec, - ) -> Self { - Self { - clock, tracks, scenes, midi_ins, midi_outs, - jack: jack.clone(), - name: name.unwrap_or_default(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, - ..Default::default() - } - } - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } -} - - -#[cfg(feature = "track")] impl Arrangement { - /// Get the active track - pub fn get_track (&self) -> Option<&Track> { - let index = self.selection().track()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active track - pub fn get_track_mut (&mut self) -> Option<&mut Track> { - let index = self.selection().track()?; - Has::>::get_mut(self).get_mut(index) - } - /// Add multiple tracks - pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - /// Add a track - pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - - pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { - Bsp::s( - Fixed::Y(1, self.view_inputs_header()), - Thunk::new(|to: &mut TuiOut|{ - for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) - } - }) - ) - } - - fn view_inputs_header (&self) -> impl Content + '_ { - Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - )))))) - }))) - } - - fn view_inputs_row (&self, port: &MidiInput) -> impl Content { - Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), - ))))) - }))) - } - - pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = Bsp::s( - Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::Y(1,Fill::X(Bsp::e( - Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - Fill::X(Align::e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::X( - Thunk::new(|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::X(track_width(index, track), - Thunk::new(|to: &mut TuiOut|{ - to.place(&Fixed::Y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, "play "), "play "), - Either::new(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::Y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, " ● "), " · "), - Either::new(false, Tui::fg(Yellow, " ● "), " · "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&Fixed::Y(1, Fill::X(""))); - } - }})))}})))))) - } - - pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::XY(track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, - |_, _index|Fixed::XY(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - Align::nw(format!(" · {}", "--"))))))))); - })) - } - -} - -#[cfg(feature = "scene")] impl Arrangement { - /// Get the active scene - pub fn get_scene (&self) -> Option<&Scene> { - let index = self.selection().scene()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active scene - pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { - let index = self.selection().scene()?; - Has::>::get_mut(self).get_mut(index) - } -} - -#[cfg(feature = "scene")] impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.measure_height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.measure_width() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} - -#[cfg(feature = "clip")] impl Arrangement { - /// Get the active clip - pub fn get_clip (&self) -> Option>> { - self.get_scene()?.clips.get(self.selection().track()?)?.clone() - } - /// Put a clip in a slot - pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - /// Change the color of a clip, returning the previous one - pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - old - }) - } - /// Toggle looping for the active clip - pub fn toggle_loop (&mut self) { - if let Some(clip) = self.get_clip() { - clip.write().unwrap().toggle_loop() - } - } -} - -#[cfg(feature = "sampler")] impl Arrangement { - /// Get the first sampler of the active track - pub fn sampler (&self) -> Option<&Sampler> { - self.get_track()?.sampler(0) - } - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.get_track_mut()?.sampler_mut(0) - } -} - -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.size_inner } -} - -impl PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - t => matches!(other, t) - } - } -} - -impl Browse { - - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { cwd, dirs, files, ..Default::default() }) - } - - pub fn len (&self) -> usize { - self.dirs.len() + self.files.len() - } - - pub fn is_dir (&self) -> bool { - self.index < self.dirs.len() - } - - pub fn is_file (&self) -> bool { - self.index >= self.dirs.len() - } - - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } - -} - -impl Browse { - fn _todo_stub_path_buf (&self) -> PathBuf { - todo!() - } - fn _todo_stub_usize (&self) -> usize { - todo!() - } - fn _todo_stub_arc_str (&self) -> Arc { - todo!() - } -} - - -impl HasContent for Browse { - fn content (&self) -> impl Content { - Map::south(1, ||EntriesIterator { - offset: 0, - index: 0, - length: self.dirs.len() + self.files.len(), - browser: self, - }, |entry, _index|Fill::X(Align::w(entry))) - } -} - -impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; - fn next (&mut self) -> Option { - let dirs = self.browser.dirs.len(); - let files = self.browser.files.len(); - let index = self.index; - if self.index < dirs { - self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) - } else if self.index < dirs + files { - self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) - } else { - None - } - } -} - -impl MidiClip { - pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().into(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemTheme::random) - } - } - pub fn count_midi_messages (&self) -> usize { - let mut count = 0; - for tick in self.notes.iter() { - count += tick.len(); - } - count - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.looped = !self.looped; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend clip first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - false - } - pub fn stop_all () -> Self { - Self::new( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) - ) - } -} - -impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -impl Eq for MidiClip {} - -impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } -} -//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - -impl> HasClock for T { - fn clock (&self) -> &Clock { self.get() } - fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } -} - -impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } -} - -impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &BeatsPerMinute { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &PulsesPerQuaver { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } -} - -impl Clock { - fn _todo_provide_u32 (&self) -> u32 { - todo!() - } - fn _todo_provide_opt_u32 (&self) -> Option { - todo!() - } - fn _todo_provide_f64 (&self) -> f64 { - todo!() - } -} - -impl Command for ClockCommand { - fn execute (&self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) // awesome - } -} - -impl Default for ClockView { - fn default () -> Self { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } - } -} - -impl ClockView { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - - //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) - //-> Arc> - //{ - //let data = (track, tracks); - //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); - //cache.read().unwrap().trks.view.clone() - //} - - //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - //-> impl Content - //{ - //let data = (scene, scenes); - //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); - //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) - //} - - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); - } - } - - //pub fn view_h2 (&self) -> impl Content { - //let cache = self.project.clock.view_cache.clone(); - //let cache = cache.read().unwrap(); - //add(&Fixed::x(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::x(13, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::x(12, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - //add(&Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( - //self.tracks(), - //self.scenes() - //))))), - //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) - //)); - ////if let Some(last) = self.history.last() { - ////add(&FieldV(theme, format!("History ({})", self.history.len()), - ////Fill::X(Align::w(format!("{:?}", last.0))))); - ////} - //} -} - - -impl>> HasEditor for T {} - -has!(Measure: |self: MidiEditor|self.size); - -impl Default for MidiEditor { - fn default () -> Self { - Self { - size: Measure::new(0, 0), - mode: PianoHorizontal::new(None), - } - } -} - -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } -} - -from!(MidiEditor: |clip: &Arc>| { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(MidiEditor: |clip: Option>>| { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos((note_end + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - pub fn clip_status (&self) -> impl Content + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("f2", "name ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("l", "ength ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("r", "epeat ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Content + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", note_pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("t", "ime ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("z", "lock ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - Fill::X(Align::w(Bsp::e( - button_2("x", "note ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) - } -} - -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} - -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} - -impl NotePoint for MidiEditor { - fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } -} - -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} - -impl Draw for MidiEditor { - fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } -} - -impl Layout for MidiEditor { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } -} - -impl HasContent for MidiEditor { - fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } -} - - -has!(Measure:|self:PianoHorizontal|self.size); - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(0, 0); - let mut range = MidiSelection::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiCursor::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -impl Draw for PianoHorizontal { - fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } -} -impl Layout for PianoHorizontal { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } -} -impl HasContent for PianoHorizontal { - fn content (&self) -> impl Content { - Bsp::s( - Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), - ) - } -} - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.rgb); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Content { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut TuiOut|{ - let source = buffer.read().unwrap(); - let XYWH(x0, y0, w, _h) = to.area(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Content { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x0, y0, w, _) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Content { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x, y0, _w, _h) = to.area(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Content + '_ { - Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x, y, w, _h) = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} - -impl Default for OctaveVertical { - fn default () -> Self { - Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } - } -} -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } -} -impl HasContent for OctaveVertical { - fn content (&self) -> impl Content + '_ { - row!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} - -impl Lv2 { - - pub fn new ( - jack: &Jack<'static>, - name: &str, - uri: &str, - ) -> Usually { - let lv2_world = livi::World::with_load_bundle(&uri); - let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let lv2_plugin = lv2_world.iter_plugins().nth(0) - .unwrap_or_else(||panic!("plugin not found: {uri}")); - Ok(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(uri).into()), - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - lv2_instance: unsafe { - lv2_plugin - .instantiate(lv2_features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - lv2_port_list: lv2_plugin.ports().collect::>(), - lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - lv2_ui_thread: None, - lv2_world, - lv2_features, - lv2_plugin, - }) - } - - const INPUT_BUFFER: usize = 1024; - -} - -audio!(|self: Lv2, _client, scope|{ - let Self { - midi_ins, - midi_outs, - audio_ins, - audio_outs, - lv2_features, - lv2_instance, - lv2_input_buffer, - .. - } = self; - let urid = lv2_features.midi_urid(); - lv2_input_buffer.clear(); - for port in midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &lv2_features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - lv2_input_buffer.push(atom); - } - let mut outputs = vec![]; - for _ in midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - lv2_features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(lv2_input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - lv2_instance.run(scope.n_frames() as usize, ports).unwrap() - }; - Control::Continue -}); - -impl Draw for Lv2 { - fn draw (&self, to: &mut TuiOut) { - let area = to.area(); - let XYWH(x, y, _, height) = area; - let mut width = 20u16; - let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = self.lv2_port_list.get(i) { - let value = if let Some(value) = self.lv2_instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - let style = if i == self.selected { - Some(Style::default().green()) - } else { - None - } ; - to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); - } else { - break - } - } - draw_header(self, to, x, y, width); - } -} - -#[cfg(feature = "lv2_gui")] -impl LV2PluginUI { - pub fn new () -> Usually { - Ok(Self { window: None }) - } -} - -#[cfg(feature = "lv2_gui")] -impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } -} -impl Layout for RmsMeter {} -impl Draw for RmsMeter { - fn draw (&self, to: &mut TuiOut) { - let XYWH(x, y, w, h) = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - } -} -impl Layout for Log10Meter {} -impl Draw for Log10Meter { - fn draw (&self, to: &mut TuiOut) { - let XYWH(x, y, w, h) = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - } -} -impl Default for Pool { - fn default () -> Self { - //use PoolMode::*; - Self { - clip: 0.into(), - mode: None, - visible: true, - #[cfg(feature = "clip")] clips: Arc::from(RwLock::from(vec![])), - #[cfg(feature = "sampler")] samples: Arc::from(RwLock::from(vec![])), - #[cfg(feature = "browse")] browse: None, - } - } -} -impl Pool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } -} -impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } -} -impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } -} -#[macro_export] macro_rules! has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} -has_clips!(|self: Pool|self.clips); -has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); -from!(Pool: |clip:&Arc>|{ - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); -impl Pool { - fn _todo_usize_ (&self) -> usize { todo!() } - fn _todo_bool_ (&self) -> bool { todo!() } - fn _todo_clip_ (&self) -> MidiClip { todo!() } - fn _todo_path_ (&self) -> PathBuf { todo!() } - fn _todo_color_ (&self) -> ItemColor { todo!() } - fn _todo_str_ (&self) -> Arc { todo!() } - fn clip_new (&self) -> MidiClip { self.new_clip() } - fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } - fn clip_index_current (&self) -> usize { 0 } - fn clip_index_after (&self) -> usize { 0 } - fn clip_index_previous (&self) -> usize { 0 } - fn clip_index_next (&self) -> usize { 0 } - fn color_random (&self) -> ItemColor { ItemColor::random() } -} -impl<'a> HasContent for PoolView<'a> { - fn content (&self) -> impl Content { - let Self(pool) = self; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - //let height = pool.clips.read().unwrap().len() as u16; - Fixed::X(20, Fill::Y(Align::n(Map::new( - ||pool.clips().clone().into_iter(), - move|clip: Arc>, i: usize|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == pool.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if false { String::default() } else { format!("{length} ") }; - Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( - Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), - Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), - Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - })))) - } -} -impl HasContent for ClipLength { - fn content (&self) -> impl Content + '_ { - use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => row!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), - } - } -} - -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} -//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} - -impl>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { - self.get() - } - fn midi_ins_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - - -impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } -} - -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} - - -impl>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { - self.get() - } - fn midi_outs_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - -/// Trail for thing that may gain new MIDI ports. -impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } -} - -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl Draw for AddSampleModal { - fn draw (&self, _to: &mut TuiOut) { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; - //for (i, (is_dir, name)) in self.subdirs.iter() - //.map(|path|(true, path)) - //.chain(self.files.iter().map(|path|(false, path))) - //.enumerate() - //.skip(self.offset) - //{ - //if i >= area.h() as usize - 4 { - //break - //} - //let t = if is_dir { "" } else { "" }; - //let line = format!("{t} {}", name.to_string_lossy()); - //let line = &line[..line.len().min(area.w() as usize - 4)]; - //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { - //Style::default().green() - //} else { - //Style::default().white() - //}))?; - //} - //Lozenge(Style::default()).draw(to) - } -} - -impl Sampler { - - pub fn view_grid (&self) -> impl Content + use<'_> { - //let cells_x = 8u16; - //let cells_y = 8u16; - //let cell_width = 10u16; - //let cell_height = 2u16; - //let width = cells_x * cell_width; - //let height = cells_y * cell_height; - //let cols = Map::east( - //cell_width, - //move||0..cells_x, - //move|x, _|Map::south( - //cell_height, - //move||0..cells_y, - //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) - //) - //); - //cols - //Thunk::new(|to: &mut TuiOut|{ - //}) - "TODO" - } - - pub fn view_grid_cell <'a> ( - &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 - ) -> impl Content + use<'a> { - let cursor = self.cursor(); - let hi_fg = Color::Rgb(64, 64, 64); - let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; - let tx_fg = if let Some((index, _)) = self.recording - && index % 8 == x as usize - && index / 8 == y as usize - { - Color::Rgb(255, 64, 0) - } else { - Color::Rgb(255, 255, 255) - }; - let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { - Color::Rgb(96, 96, 96) - } else { - Color::Rgb(64, 64, 64) - }; - let lo_fg = Color::Rgb(64, 64, 64); - let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; - Fixed::XY(w, h, Bsp::s( - Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, Repeat::X(Phat::<()>::LO))), - Bsp::n( - Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, Repeat::X(Phat::<()>::HI))), - Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), - ), - )) - } - - const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; - - pub fn view_list <'a, T: NotePoint + NoteRange> ( - &'a self, compact: bool, editor: &T - ) -> impl Content + 'a { - let note_lo = editor.get_note_lo(); - let note_pt = editor.get_note_pos(); - let note_hi = editor.get_note_hi(); - Fixed::X(if compact { 4 } else { 12 }, Map::south( - 1, - move||(note_lo..=note_hi).rev(), - move|note, _index| { - //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); - let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; - let mut fg = Tui::g(160); - if let Some(mapped) = &self.mapped[note] { - let sample = mapped.read().unwrap(); - fg = if note == note_pt { - sample.color.lightest.rgb - } else { - Tui::g(224) - }; - bg = if note == note_pt { - sample.color.light.rgb - } else { - sample.color.base.rgb - }; - } - if let Some((index, _)) = self.recording { - if note == index { - bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; - fg = Color::Rgb(224,64,32) - } - } - Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) - })) - } - - pub fn view_list_item (&self, note: usize, compact: bool) -> String { - if compact { - String::default() - } else { - draw_list_item(&self.mapped[note]) - } - } - - pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - }))) - } - - pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { - Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - }))) - } - - pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { - Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - })) - } - - pub fn view_status (&self, index: usize) -> impl Content { - draw_status(self.mapped[index].as_ref()) - } - - pub fn view_meters_input (&self) -> impl Content + use<'_> { - draw_meters(&self.input_meters) - } - - pub fn view_meters_output (&self) -> impl Content + use<'_> { - draw_meters(&self.output_meters) - } -} - -fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { - Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ - Fill::Y(RmsMeter(*value)) - }))) -} - -fn draw_list_item (sample: &Option>>) -> String { - if let Some(sample) = sample { - let sample = sample.read().unwrap(); - format!("{:8}", sample.name) - //format!("{:8} {:3} {:6}-{:6}/{:6}", - //sample.name, - //sample.gain, - //sample.start, - //sample.end, - //sample.channels[0].len() - //) - } else { - String::from("........") - } -} - -fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { - let min_db = -64.0; - Thunk::new(move|to: &mut TuiOut|{ - let XYWH(x, y, width, height) = to.area(); - let area = Rect { x, y, width, height }; - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - Canvas::default() - .x_bounds([sample.start as f64, sample.end as f64]) - .y_bounds([min_db, 0.]) - .paint(|ctx| { - for line in lines.iter() { - ctx.draw(line); - } - //FIXME: proportions - //let text = "press record to finish sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }).render(area, &mut to.buffer); - } else { - Canvas::default() - .x_bounds([0.0, width as f64]) - .y_bounds([0.0, height as f64]) - .paint(|_ctx| { - //let text = "press record to begin sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, &mut to.buffer); - } - }) -} - -impl Sampler { - fn sample_selected (&self) -> usize { - (self.get_note_pos() as u8).into() - } - fn sample_selected_pitch (&self) -> u7 { - (self.get_note_pos() as u8).into() - } -} - -impl AddSampleModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } -} - -impl AddSampleModal { - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -impl Sampler { - - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - self.reset_input_meters(); - if self.recording.is_some() { - self.record_into(scope); - } else { - self.update_input_meters(scope); - } - } - - /// Make sure that input meter count corresponds to input channel count - fn reset_input_meters (&mut self) { - let channels = self.audio_ins.len(); - if self.input_meters.len() != channels { - self.input_meters = vec![f32::MIN;channels]; - } - } - - /// Record from inputs to sample - fn record_into (&mut self, scope: &ProcessScope) { - if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != self.audio_ins.len() { - panic!("channel count mismatch"); - } - let samples_with_meters = self.audio_ins.iter() - .zip(self.input_meters.iter_mut()) - .zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in samples_with_meters { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - *meter = to_rms(slice); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - panic!("tried to record into the void") - } - } - - /// Update input meters - fn update_input_meters (&mut self, scope: &ProcessScope) { - for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { - let slice = input.port().as_slice(scope); - *meter = to_rms(slice); - } - } - - /// Make sure that output meter count corresponds to input channel count - fn reset_output_meters (&mut self) { - let channels = self.audio_outs.len(); - if self.output_meters.len() != channels { - self.output_meters = vec![f32::MIN;channels]; - } - } - - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - self.clear_output_buffer(); - self.populate_output_buffer(scope.n_frames() as usize); - self.write_output_buffer(scope); - } - - /// Zero the output buffer. - fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - - /// Write playing voices to output buffer - fn populate_output_buffer (&mut self, frames: usize) { - let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; - let channel_count = buffer.len(); - match mixing_mode { - MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ - mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ - mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - } - } - - /// Write output buffer to output ports. - fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} - -impl Sampler { - pub fn new ( - jack: &Jack<'static>, - name: impl AsRef, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] audio_from: &[&[Connect];2], - #[cfg(feature = "port")] audio_to: &[&[Connect];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - #[cfg(feature = "port")] midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, - #[cfg(feature = "port")] audio_ins: vec![ - AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, - AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - #[cfg(feature = "port")] audio_outs: vec![ - AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, - AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - input_meters: vec![0.0;2], - output_meters: vec![0.0;2], - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - mode: None, - editing: None, - size: Default::default(), - note_lo: 0.into(), - note_pt: 0.into(), - cursor: (0.into(), 0.into()), - color: Default::default(), - mixing_mode: Default::default(), - metering_mode: Default::default(), - }) - } - /// Value of cursor - pub fn cursor (&self) -> (usize, usize) { - (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) - } -} - -impl NoteRange for Sampler { - fn note_lo (&self) -> &AtomicUsize { - &self.note_lo - } - fn note_axis (&self) -> &AtomicUsize { - &self.size.y - } -} - -impl NotePoint for Sampler { - fn note_len (&self) -> &AtomicUsize { - unreachable!(); - } - fn get_note_len (&self) -> usize { - 0 - } - fn set_note_len (&self, x: usize) -> usize { - 0 /*TODO?*/ - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pt - } - fn get_note_pos (&self) -> usize { - self.note_pt.load(Relaxed) - } - fn set_note_pos (&self, x: usize) -> usize { - let old = self.note_pt.swap(x, Relaxed); - self.cursor.0.store(x % 8, Relaxed); - self.cursor.1.store(x / 8, Relaxed); - old - } -} - -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } -} - -impl Sample { - - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } - -} - -impl Sampler { - - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = self; - for RawMidi { time, bytes } in midi_in.port().iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller: _, value: _ } => { - // TODO - } - _ => {} - } - } - } - } - -} - -impl Sample { - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } -} - -audio!(|self: Sampler, _client, scope|{ - self.process_midi_in(scope); - self.process_audio_out(scope); - self.process_audio_in(scope); - Control::Continue -}); - -impl> + Send + Sync> HasScenes for T {} -impl HasSceneScroll for Arrangement { - fn scene_scroll (&self) -> usize { self.scene_scroll } -} - -impl AddScene for T {} - - -impl Scene { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -impl> + Send + Sync> HasScene for T {} - - -impl Scene { - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} - -impl> HasSelection for T {} - -#[cfg(feature = "track")] -impl Selection { - pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } -} - -#[cfg(feature = "scene")] -impl Selection { - pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } -} - -impl Selection { - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } -} - -impl> HasSequencer for T { - fn sequencer (&self) -> &Sequencer { - self.get() - } - fn sequencer_mut (&mut self) -> &mut Sequencer { - self.get_mut() - } -} - -impl Default for Sequencer { - fn default () -> Self { - Self { - #[cfg(feature = "clock")] clock: Clock::default(), - #[cfg(feature = "clip")] play_clip: None, - #[cfg(feature = "clip")] next_clip: None, - #[cfg(feature = "port")] midi_ins: vec![], - #[cfg(feature = "port")] midi_outs: vec![], - - recording: false, - monitoring: true, - overdub: false, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - midi_buf: vec![], - reset: true, - } - } -} - -impl Sequencer { - pub fn new ( - name: impl AsRef, - jack: &Jack<'static>, - #[cfg(feature = "clock")] clock: Option<&Clock>, - #[cfg(feature = "clip")] clip: Option<&Arc>>, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] midi_to: &[Connect], - ) -> Usually { - let _name = name.as_ref(); - #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); - Ok(Self { - reset: true, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], - #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], - #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), - #[cfg(feature = "clock")] clock, - ..Default::default() - }) - } -} - -impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } -} - -#[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); -#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); - -impl MidiMonitor for Sequencer { - fn notes_in (&self) -> &Arc> { - &self.notes_in - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } -} - -impl MidiRecord for Sequencer { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } -} - -#[cfg(feature="clip")] impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } -} - -/// JACK process callback for a sequencer's clip sequencer/recorder. -impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } -} - -impl Sequencer { - fn process_rolling (&mut self, scope: &ProcessScope) -> Control { - self.process_clear(scope, false); - // Write chunk of clip to output, handle switchover - if self.process_playback(scope) { - self.process_switchover(scope); - } - // Monitor input to output - self.process_monitoring(scope); - // Record and/or monitor input - self.process_recording(scope); - // Emit contents of MIDI buffers to JACK MIDI output ports. - self.midi_outs_emit(scope); - Control::Continue - } - fn process_stopped (&mut self, scope: &ProcessScope) -> Control { - if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { - self.process_monitoring(scope) - } - Control::Continue - } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat - let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.write().unwrap(), &message); - } - } - } - } - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); - for frame in &mut self.midi_buf_mut()[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(self.midi_buf_mut()); - } - for port in self.midi_outs_mut().iter_mut() { - // Clear output buffer(s) - port.buffer_clear(scope, false); - } - } - fn process_recording (&mut self, scope: &ProcessScope) { - if self.monitoring() { - self.monitor(scope); - } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); - } - if let Some((_start_at, _clip)) = &self.next_clip() { - self.record_next(); - } - } - fn process_playback (&mut self, scope: &ProcessScope) -> bool { - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = &self.play_clip { - // Length of clip, to repeat or stop on end. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Index of first sample to populate. - let offset = self.clock().get_sample_offset(scope, &started); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.clock().get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(clip) = clip { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - for port in self.midi_outs.iter_mut() { - port.buffer_write(sample, LiveEvent::Midi { - channel: 0.into(), /* TODO */ - message: *message - }); - } - } - } - } - } - false - } else { - true - } - } - /// Handle switchover from current to next playing clip. - fn process_switchover (&mut self, scope: &ProcessScope) { - let midi_buf = self.midi_buf_mut(); - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.process_playback(scope); - } - } - } -} - -impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { - &mut self.note_buf - } - fn midi_buf_mut (&mut self) -> &mut Vec>> { - &mut self.midi_buf - } -} - -impl> + Send + Sync> HasTracks for T {} - -impl HasTrackScroll for Arrangement { - fn track_scroll (&self) -> usize { - self.track_scroll - } -} - -impl> HasTrack for T { - fn track (&self) -> Option<&Track> { - self.get() - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.get_mut() - } -} - -has!(Clock: |self: Track|self.sequencer.clock); -has!(Sequencer: |self: Track|self.sequencer); -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, - ..Default::default() - }) - } - pub fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - pub fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } -} - -#[cfg(feature = "sampler")] -impl Track { - /// Create a new track connecting the [Sequencer] to a [Sampler]. - pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } -} - -impl ClipsView for T {} - -impl Track { - pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}) - } -} - diff --git a/device/device_struct.rs b/device/device_struct.rs deleted file mode 100644 index ba2b3c90..00000000 --- a/device/device_struct.rs +++ /dev/null @@ -1,633 +0,0 @@ -use crate::*; - -/// The source of time. -/// -/// ``` -/// let clock = tek_device::Clock::default(); -/// ``` -#[derive(Clone, Default)] pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -/// Contains memoized renders of clock values. -/// -/// Performance optimization. -#[derive(Debug)] pub struct ClockView { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - -/// Arranger. -/// -/// ``` -/// let arranger = tek_device::Arrangement::default(); -/// ``` -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// 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>, - /// Display size - pub size: Measure, - /// Display size of clips area - pub size_inner: Measure, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// 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, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub 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, -} - -#[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>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -/// 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, - /// 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, - /// 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>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// 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, - pub path: Option>, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, - - pub lv2_world: livi::World, - pub lv2_instance: livi::Instance, - pub lv2_plugin: livi::Plugin, - pub lv2_features: Arc, - pub lv2_port_list: Vec, - pub lv2_input_buffer: Vec, - pub lv2_ui_thread: Option>, -} - -/// A LV2 plugin's X11 UI. -#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { - pub window: Option -} - -#[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, - /// Embedded file browse - #[cfg(feature = "browse")] pub browse: Option, - /// Collection of MIDI clips. - #[cfg(feature = "clip")] pub clips: Arc>>>>, - /// Collection of sound samples. - #[cfg(feature = "sampler")] pub samples: Arc>>>>, -} - -/// 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, -} - -/// 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, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// 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, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// 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, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// 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, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek_engine::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -/// Plays [Voice]s from [Sample]s. -/// -/// ``` -/// let sampler = tek_device::Sampler::default(); -/// ``` -#[derive(Debug, Default)] pub struct Sampler { - /// Name of sampler. - pub name: Arc, - /// Device color. - pub color: ItemTheme, - /// Audio input ports. Samples get recorded here. - #[cfg(feature = "port")] pub audio_ins: Vec, - /// Audio input meters. - #[cfg(feature = "meter")] pub input_meters: Vec, - /// Sample currently being recorded. - pub recording: Option<(usize, Option>>)>, - /// Recording buffer. - pub buffer: Vec>, - /// Samples mapped to MIDI notes. - pub mapped: [Option>>;128], - /// Samples that are not mapped to MIDI notes. - pub unmapped: Vec>>, - /// Sample currently being edited. - pub editing: Option>>, - /// MIDI input port. Triggers sample playback. - #[cfg(feature = "port")] pub midi_in: Option, - /// Collection of currently playing instances of samples. - pub voices: Arc>>, - /// Audio output ports. Voices get played here. - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Audio output meters. - #[cfg(feature = "meter")] pub output_meters: Vec, - /// 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, - /// Size of rendered sampler. - pub size: Measure, - /// 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, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -pub struct AddSampleModal { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, -} - -/// 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, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -/// 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>>)>, - /// Start time and next clip - #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, - /// Record from MIDI ports to current sequence. - #[cfg(feature = "port")] pub midi_ins: Vec, - /// Play from current sequence to MIDI ports - #[cfg(feature = "port")] pub midi_outs: Vec, - /// 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>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, - /// MIDI output buffer - pub midi_buf: Vec>>, -} - -/// 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, - /// 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, -} - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} - -/// Modes for clip pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// 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), - /** Match regular expression */ - RegExp(Arc), -} - -#[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), -} diff --git a/device/device_type.rs b/device/device_type.rs deleted file mode 100644 index 4455b3cb..00000000 --- a/device/device_type.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::*; - -pub type MidiData = Vec>; - -pub type ClipPool = Vec>>; - -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; - -pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); - -pub type MidiSample = (Option, Arc>); diff --git a/engine/Cargo.toml b/engine/Cargo.toml deleted file mode 100644 index 23988f80..00000000 --- a/engine/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "tek_engine" -edition = { workspace = true } -version = { workspace = true } - -[lib] -path = "engine.rs" - -[target.'cfg(target_os = "linux")'] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[dependencies] -atomic_float = { workspace = true } -jack = { workspace = true } -midly = { workspace = true } -tengri = { workspace = true } - -[dev-dependencies] -proptest = { workspace = true } -proptest-derive = { workspace = true } - -[features] diff --git a/engine/engine.rs b/engine/engine.rs deleted file mode 100644 index 0299c67b..00000000 --- a/engine/engine.rs +++ /dev/null @@ -1,196 +0,0 @@ -pub extern crate jack; -pub extern crate midly; -pub extern crate tengri; -mod engine_structs; pub use self::engine_structs::*; -mod engine_traits; pub use self::engine_traits::*; -mod engine_impls; -mod jack_structs; pub use self::jack_structs::*; -mod jack_traits; pub use self::jack_traits::*; -mod jack_types; pub use self::jack_types::*; -mod jack_impls; -pub(crate) use ::{ - tengri::{Usually, from, output::PerfModel}, - atomic_float::AtomicF64, - std::{ - fmt::Debug, ops::{Add, Sub, Mul, Div, Rem}, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - }, -}; -pub use ::{ - jack::{*, contrib::{*, ClosureProcessHandler}}, - midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*,} -}; - -pub const DEFAULT_PPQ: f64 = 96.0; - -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; - -/// (pulses, name), assuming 96 PPQ -pub const NOTE_DURATIONS: [(usize, &str);26] = [ - (1, "1/384"), (2, "1/192"), - (3, "1/128"), (4, "1/96"), - (6, "1/64"), (8, "1/48"), - (12, "1/32"), (16, "1/24"), - (24, "1/16"), (32, "1/12"), - (48, "1/8"), (64, "1/6"), - (96, "1/4"), (128, "1/3"), - (192, "1/2"), (256, "2/3"), - (384, "1/1"), (512, "4/3"), - (576, "3/2"), (768, "2/1"), - (1152, "3/1"), (1536, "4/1"), - (2304, "6/1"), (3072, "8/1"), - (3456, "9/1"), (6144, "16/1"), -]; - -pub const NOTE_NAMES: [&str; 128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", -]; - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) - -> Box, &'a [u8])> + 'a> -{ - Box::new(input.map(|::jack::RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -/// Returns the next shorter length -pub fn note_duration_prev (pulses: usize) -> usize { - for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } - pulses -} - -/// Returns the next longer length -pub fn note_duration_next (pulses: usize) -> usize { - for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } - pulses -} - -pub fn note_duration_to_name (pulses: usize) -> &'static str { - for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } - "" -} - -pub fn note_pitch_to_name (n: usize) -> &'static str { - if n > 127 { - panic!("to_note_name({n}): must be 0-127"); - } - NOTE_NAMES[n] -} - - -//macro_rules! impl_port { - //($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { - //#[derive(Debug)] pub struct $Name { - ///// Handle to JACK client, for receiving reconnect events. - //jack: Jack<'static>, - ///// Port name - //name: Arc, - ///// Port handle. - //port: Port<$Spec>, - ///// List of ports to connect to. - //conn: Vec - //} - //impl AsRef> for $Name { - //fn as_ref (&self) -> &Port<$Spec> { &self.port } - //} - //impl $Name { - //pub fn new ($jack: &Jack, name: impl AsRef, connect: &[PortConnect]) - //-> Usually - //{ - //let $name = name.as_ref(); - //let jack = $jack.clone(); - //let port = $port?; - //let name = $name.into(); - //let conn = connect.to_vec(); - //let port = Self { jack, port, name, conn }; - //port.connect_to_matching()?; - //Ok(port) - //} - //pub fn name (&self) -> &Arc { &self.name } - //pub fn port (&self) -> &Port<$Spec> { &self.port } - //pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port } - //pub fn into_port (self) -> Port<$Spec> { self.port } - //pub fn close (self) -> Usually<()> { - //let Self { jack, port, .. } = self; - //Ok(jack.with_client(|client|client.unregister_port(port))?) - //} - //} - //impl HasJack<'static> for $Name { - //fn jack (&self) -> &'static Jack<'static> { &self.jack } - //} - //impl JackPort<'static> for $Name { - //type Port = $Spec; - //type Pair = $Pair; - //fn port (&self) -> &Port<$Spec> { &self.port } - //} - //impl ConnectTo<'static, &str> for $Name { - //fn connect_to (&self, to: &str) -> Usually { - //self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { - //self.connect_to(port) - //} else { - //Ok(Missing) - //}) - //} - //} - //impl ConnectTo<'static, &Port> for $Name { - //fn connect_to (&self, port: &Port) -> Usually { - //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 - //})) - //} - //} - //impl ConnectTo<'static, &Port<$Pair>> for $Name { - //fn connect_to (&self, port: &Port<$Pair>) -> Usually { - //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 - //})) - //} - //} - //impl ConnectAuto<'static> for $Name { - //fn connections (&self) -> &[PortConnect] { - //&self.conn - //} - //} - //}; -//} diff --git a/engine/engine_impls.rs b/engine/engine_impls.rs deleted file mode 100644 index 42f2d9e9..00000000 --- a/engine/engine_impls.rs +++ /dev/null @@ -1,363 +0,0 @@ -use crate::*; - -impl Gettable for AtomicBool { - fn get (&self) -> bool { - self.load(Relaxed) - } -} - -impl InteriorMutable for AtomicBool { - fn set (&self, value: bool) -> bool { - self.swap(value, Relaxed) - } -} - -impl Gettable for AtomicUsize { - fn get (&self) -> usize { - self.load(Relaxed) - } -} - -impl InteriorMutable for AtomicUsize { - fn set (&self, value: usize) -> usize { - self.swap(value, Relaxed) - } -} - -//impl>> MaybeHas for U { - //fn get (&self) -> Option<&T> { - //Has::>::get(self).as_ref() - //} -//} - -impl Default for MidiCursor { - fn default () -> Self { - Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), - } - } -} - -impl NotePoint for MidiCursor { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } -} - -impl TimePoint for MidiCursor { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } -} - -impl MidiPoint for T {} - -from!(MidiSelection: |data:(usize, bool)| Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), -}); - -impl MidiRange for T {} - -impl TimeRange for MidiSelection { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } -} - -impl NoteRange for MidiSelection { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } -} - -impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> Arc { - self.timebase.format_beats_1(self.pulse.get()).into() - } -} - -impl LaunchSync { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } -} - -impl Quantize { - pub fn next (&self) -> f64 { - note_duration_next(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - note_duration_prev(self.get() as usize) as f64 - } -} - -impl Iterator for TicksIterator { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.sample > self.end { return None } - let spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } -} - -impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { - TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4).into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { - let mut string = String::with_capacity(16); - self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); - string.into() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() - } -} - -impl Default for Timebase { - fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } -} -impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } -} - -impl Microsecond { - #[inline] pub fn format_msu (&self) -> Arc { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}").into() - } -} - -/// Implement an arithmetic operation for a unit of time -#[macro_export] macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} - -/// Define and implement a unit of time -#[macro_export] macro_rules! impl_time_unit { - ($T:ident) => { - impl Gettable for $T { - fn get (&self) -> f64 { self.0.load(Relaxed) } - } - impl InteriorMutable for $T { - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Relaxed); - old - } - } - impl TimeUnit for $T {} - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } - impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } - impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } - impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - } -} - -impl_time_unit!(SampleCount); -impl_time_unit!(SampleRate); -impl_time_unit!(Microsecond); -impl_time_unit!(Quantize); -impl_time_unit!(PulsesPerQuaver); -impl_time_unit!(Pulse); -impl_time_unit!(BeatsPerMinute); -impl_time_unit!(LaunchSync); diff --git a/engine/engine_structs.rs b/engine/engine_structs.rs deleted file mode 100644 index f47c64ca..00000000 --- a/engine/engine_structs.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::*; - -/// 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. -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default)] pub struct TicksIterator { - pub spp: f64, - pub sample: usize, - pub start: usize, - pub end: usize, -} - -/// -/// ``` -/// let model = 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, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -/// -/// ``` -/// -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiSelection { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -/// -/// ``` -/// -/// ``` -#[derive(Debug, Default, Clone)] -pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} - -/// -/// ``` -/// -/// ``` -#[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); diff --git a/engine/engine_traits.rs b/engine/engine_traits.rs deleted file mode 100644 index 2871e0d8..00000000 --- a/engine/engine_traits.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::*; -use std::sync::atomic::Ordering; - -//pub trait MaybeHas: Send + Sync { - //fn get (&self) -> Option<&T>; -//} - -pub trait HasN: Send + Sync { - fn get_nth (&self, key: usize) -> &T; - fn get_nth_mut (&mut self, key: usize) -> &mut T; -} - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn get_time_len (&self) -> usize { - self.time_len().load(Ordering::Relaxed) - } - fn time_zoom (&self) -> &AtomicUsize; - fn get_time_zoom (&self) -> usize { - self.time_zoom().load(Ordering::Relaxed) - } - fn set_time_zoom (&self, value: usize) -> usize { - self.time_zoom().swap(value, Ordering::Relaxed) - } - fn time_lock (&self) -> &AtomicBool; - fn get_time_lock (&self) -> bool { - self.time_lock().load(Ordering::Relaxed) - } - fn set_time_lock (&self, value: bool) -> bool { - self.time_lock().swap(value, Ordering::Relaxed) - } - fn time_start (&self) -> &AtomicUsize; - fn get_time_start (&self) -> usize { - self.time_start().load(Ordering::Relaxed) - } - fn set_time_start (&self, value: usize) -> usize { - self.time_start().swap(value, Ordering::Relaxed) - } - fn time_axis (&self) -> &AtomicUsize; - fn get_time_axis (&self) -> usize { - self.time_axis().load(Ordering::Relaxed) - } - fn get_time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() - } -} - -pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn get_note_lo (&self) -> usize { - self.note_lo().load(Ordering::Relaxed) - } - fn set_note_lo (&self, x: usize) -> usize { - self.note_lo().swap(x, Ordering::Relaxed) - } - fn note_axis (&self) -> &AtomicUsize; - fn get_note_axis (&self) -> usize { - self.note_axis().load(Ordering::Relaxed) - } - fn get_note_hi (&self) -> usize { - (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) - } -} - -pub trait MidiRange: TimeRange + NoteRange {} - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit: InteriorMutable {} diff --git a/engine/jack_impls.rs b/engine/jack_impls.rs deleted file mode 100644 index e6ae7f70..00000000 --- a/engine/jack_impls.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::*; -use JackState::*; - -/// Implement [Jack] constructor and methods -impl<'j> Jack<'j> { - - /// Register new [Client] and wrap it for shared use. - pub fn new_run + Audio + Send + Sync + 'static> ( - name: &impl AsRef, - init: impl FnOnce(Jack<'j>)->Usually - ) -> Usually>> { - Jack::new(name)?.run(init) - } - - pub fn new (name: &impl AsRef) -> Usually { - let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; - Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) - } - - pub fn run + Audio + Send + Sync + 'static> - (self, init: impl FnOnce(Self)->Usually) -> Usually>> - { - let client_state = self.0.clone(); - let app: Arc> = Arc::new(RwLock::new(init(self)?)); - let mut state = Activating; - std::mem::swap(&mut*client_state.write().unwrap(), &mut state); - if let Inactive(client) = state { - // This is the misc notifications handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [JackEvent], which is - // one of the available misc notifications. - let notify = JackNotify(Box::new({ - let app = app.clone(); - move|event|(&mut*app.write().unwrap()).handle(event) - }) as BoxedJackEventHandler); - // This is the main processing handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [Client] and [ProcessScope] - // and passes them down to the `app`'s `process` callback, which in turn - // implements audio and MIDI input and output on a realtime basis. - let process = ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &_, s: &_|if let Ok(mut app) = app.write() { - app.process(c, s) - } else { - Control::Quit - } - }) as BoxedAudioHandler); - // Launch a client with the two handlers. - *client_state.write().unwrap() = Active( - client.activate_async(notify, process)? - ); - } else { - unreachable!(); - } - Ok(app) - } - - /// Run something with the client. - pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - match &*self.0.read().unwrap() { - Inert => panic!("jack client not activated"), - Inactive(client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(client) => op(client.as_client()), - } - } -} - -impl<'j> HasJack<'j> for Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} - -impl<'j> HasJack<'j> for &Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} - -impl NotificationHandler for JackNotify { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} - -impl JackPerfModel for PerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Relaxed, - ); - self.window.store( - scope.cycle_times().unwrap().period_usecs as f64, - Relaxed, - ); - } - } -} diff --git a/engine/jack_structs.rs b/engine/jack_structs.rs deleted file mode 100644 index 5cefc9b6..00000000 --- a/engine/jack_structs.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::*; - -/// Wraps [JackState], and through it [jack::Client] when connected. -/// -/// ``` -/// let jack: tek_engine::Jack = Default::default(); -/// ``` -#[derive(Clone, Debug, Default)] -pub struct Jack<'j> (pub(crate) Arc>>); - -/// 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_engine::JackState = Default::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_engine::JackState = JackEvent::XRun; -/// ``` -#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Generic notification handler that emits [JackEvent] -/// -/// ``` -/// let notify = tek_engine::JackNotify(|_: JackEvent|{}); -/// ``` -pub struct JackNotify(pub T); diff --git a/engine/jack_traits.rs b/engine/jack_traits.rs deleted file mode 100644 index 5614747a..00000000 --- a/engine/jack_traits.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::*; - -/// Things that can provide a [jack::Client] reference. -/// -/// ``` -/// #[derive(Default)] struct Jacked<'j>(tek_engine::Jack<'j>); -/// -/// impl tek_engine::HasJack<'j> for Jacked<'j> { -/// fn jack (&self) -> &Jack<'j> { &self.0 } -/// } -/// -/// let jack: &Jack<'j> = Jacked::default().jack(); -/// ``` -pub trait HasJack<'j>: Send + Sync { - - /// Return the internal [jack::Client] handle - /// that lets you call the JACK API. - fn jack (&self) -> &Jack<'j>; - - fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - self.jack().with_client(op) - } - - fn port_by_name (&self, name: &str) -> Option> { - self.with_client(|client|client.port_by_name(name)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn register_port (&self, name: impl AsRef) -> Usually> { - self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) - } - - fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> { - if enable { - self.with_client(|client|match client.register_timebase_callback(false, callback) { - Ok(_) => Ok(()), - Err(e) => Err(e) - })? - } - Ok(()) - } - - fn sync_follow (&self, _enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } -} - -/// Trait for thing that has a JACK process callback. -pub trait Audio { - - /// Handle a JACK event. - fn handle (&mut self, _event: JackEvent) {} - - /// Projecss a JACK chunk. - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - - /// The JACK process callback function passed to the server. - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } - -} - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! audio { - - (| - $self1:ident: - $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident - |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } - $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? - } - }; - - ($Struct:ident: $process:ident, $handle:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - #[inline] fn handle (&mut self, e: JackEvent) { - $handle(self, e) - } - } - }; - -} - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -}