diff --git a/crates/cli/src/cli_groovebox.rs b/crates/cli/src/cli_groovebox.rs index 49631ec1..04ef82f1 100644 --- a/crates/cli/src/cli_groovebox.rs +++ b/crates/cli/src/cli_groovebox.rs @@ -29,15 +29,8 @@ pub struct GrooveboxCli { } impl GrooveboxCli { fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_groovebox")?.activate_with(|jack|{ - let mut app = tek::GrooveboxTui::try_from(jack)?; - let jack = jack.read().unwrap(); - let midi_in = jack.register_port("i", MidiIn::default())?; - let midi_out = jack.register_port("o", MidiOut::default())?; - app.player.midi_ins.push(midi_in); - app.player.midi_outs.push(midi_out); - Ok(app) - })?)?; + Tui::run(JackClient::new("tek_groovebox")? + .activate_with(|jack|tek::GrooveboxTui::try_from(jack))?)?; Ok(()) } } diff --git a/crates/tek/src/groovebox.rs b/crates/tek/src/groovebox.rs index 64a444fe..2070a41a 100644 --- a/crates/tek/src/groovebox.rs +++ b/crates/tek/src/groovebox.rs @@ -8,51 +8,58 @@ use PhrasePoolCommand::*; pub struct GrooveboxTui { _jack: Arc>, - pub clock: ClockModel, - pub phrases: PoolModel, + pub player: MidiPlayer, + pub pool: PoolModel, pub editor: MidiEditorModel, + pub sampler: crate::sampler::Sampler, + pub size: Measure, pub status: bool, pub note_buf: Vec, pub midi_buf: Vec>>, pub perf: PerfModel, - pub sampler: SamplerTui, } from_jack!(|jack|GrooveboxTui { - let clock = ClockModel::from(jack); + let mut player = MidiPlayer::new(jack, "sequencer")?; let phrase = Arc::new(RwLock::new(MidiClip::new( - "New", true, 4 * clock.timebase.ppq.get() as usize, + "New", true, 4 * player.clock.timebase.ppq.get() as usize, None, Some(ItemColor::random().into()) ))); - let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; - let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; - let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; - let audio_in_1 = jack.read().unwrap().register_port("inL", AudioIn::default())?; - let audio_in_2 = jack.read().unwrap().register_port("inR", AudioIn::default())?; - let audio_out_1 = jack.read().unwrap().register_port("out1", AudioOut::default())?; - let audio_out_2 = jack.read().unwrap().register_port("out2", AudioOut::default())?; + player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone()))); + let pool = crate::pool::PoolModel::from(&phrase); + let editor = crate::midi::MidiEditorModel::from(&phrase); + let sampler = crate::sampler::Sampler::new(jack, "sampler")?; Self { - _jack: jack.clone(), - phrases: PoolModel::from(&phrase), - editor: MidiEditorModel::from(&phrase), - player: MidiPlayer::from((&clock, &phrase)), - sampler: SamplerTui::try_from(jack)?, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - status: true, - clock, + _jack: jack.clone(), + player, + pool, + editor, + sampler, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + status: true, } }); -audio!(|self:GrooveboxTui,_client,_process|Control::Continue); -has_clock!(|self:GrooveboxTui|&self.clock); +audio!(|self: GrooveboxTui, client, scope|{ + let t0 = self.perf.get_t0(); + if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { + return Control::Quit + } + if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { + return Control::Quit + } + self.perf.update(t0, scope); + Control::Continue +}); +has_clock!(|self:GrooveboxTui|&self.player.clock); render!(|self:GrooveboxTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.phrases.visible { phrase_w } else { 0 }; + let pool_w = if self.pool.visible { phrase_w } else { 0 }; let sampler_w = 24; Fill::wh(lay!([ &self.size, @@ -69,12 +76,15 @@ render!(|self:GrooveboxTui|{ PhraseSelector::next_phrase(&self.player), ]))), row!([ - Tui::pull_y(1, Tui::shrink_y(0, Fill::h(Fixed::w(sampler_w, &self.sampler)))), Tui::split_n(false, 1, MidiEditStatus(&self.editor), Tui::split_w(false, pool_w, - Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.phrases)))), - Fill::wh(&self.editor) + Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.pool)))), + col!([ + &format!("L/{:>+10.3}", self.sampler.input_meter[0]), + &format!("R/{:>+10.3}", self.sampler.input_meter[1]), + Fill::wh(&self.editor) + ]) ) ), ]), @@ -110,16 +120,16 @@ input_to_command!(GrooveboxCommand: |state: GrooveboxTui, input|match input ), // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => Pool(PoolCommand::Show(!state.phrases.visible)), + key_pat!(Tab) => Pool(PoolCommand::Show(!state.pool.visible)), // q: Enqueue currently edited phrase - key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), + key_pat!(Char('q')) => Enqueue(Some(state.pool.phrase().clone())), // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), + key_pat!(Char('0')) => Enqueue(Some(state.pool.phrases()[0].clone())), // e: Toggle between editing currently playing or other phrase key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); - let selected = state.phrases.phrase().clone(); + let selected = state.pool.phrase().clone(); Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { selected } else { @@ -133,7 +143,7 @@ input_to_command!(GrooveboxCommand: |state: GrooveboxTui, input|match input // The ones defined above supersede them. _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { Editor(command) - } else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) { + } else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { Pool(command) } else { return None @@ -143,19 +153,19 @@ input_to_command!(GrooveboxCommand: |state: GrooveboxTui, input|match input command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { Self::Pool(cmd) => { let mut default = |cmd: PoolCommand|cmd - .execute(&mut state.phrases) + .execute(&mut state.pool) .map(|x|x.map(Pool)); match cmd { // autoselect: automatically load selected phrase in editor PoolCommand::Select(_) => { let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); + state.editor.set_phrase(Some(state.pool.phrase())); undo }, // update color in all places simultaneously PoolCommand::Phrase(SetColor(index, _)) => { let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); + state.editor.set_phrase(Some(state.pool.phrase())); undo }, _ => default(cmd)? diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs index c242ed61..da2f3018 100644 --- a/crates/tek/src/midi.rs +++ b/crates/tek/src/midi.rs @@ -126,6 +126,31 @@ pub struct MidiPlayer { /// MIDI output buffer pub note_buf: Vec, } +impl MidiPlayer { + pub fn new (jack: &Arc>, name: &str) -> Usually { + Ok(Self { + clock: ClockModel::from(jack), + play_phrase: None, + next_phrase: None, + recording: false, + monitoring: false, + overdub: false, + + notes_in: RwLock::new([false;128]).into(), + midi_ins: vec![ + jack.midi_in(&format!("M/{name}"))?, + ], + + midi_outs: vec![ + jack.midi_out(&format!("{name}/M"))?, + ], + notes_out: RwLock::new([false;128]).into(), + reset: true, + + note_buf: vec![0;8], + }) + } +} impl std::fmt::Debug for MidiPlayer { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("MidiPlayer") diff --git a/crates/tek/src/sampler.rs b/crates/tek/src/sampler.rs index 0ffff68a..195b3109 100644 --- a/crates/tek/src/sampler.rs +++ b/crates/tek/src/sampler.rs @@ -43,6 +43,7 @@ pub struct Sampler { pub voices: Arc>>, pub midi_in: Port, pub audio_ins: Vec>, + pub input_meter: Vec, pub audio_outs: Vec>, pub buffer: Vec>, pub output_gain: f32 @@ -50,11 +51,12 @@ pub struct Sampler { impl Sampler { pub fn new (jack: &Arc>, name: &str) -> Usually { Ok(Self { - midi_in: jack.midi_in(&format!("M->{name}"))?, + midi_in: jack.midi_in(&format!("M/{name}"))?, audio_ins: vec![ jack.audio_in(&format!("L/{name}"))?, jack.audio_in(&format!("R/{name}"))? ], + input_meter: vec![0.0;2], audio_outs: vec![ jack.audio_out(&format!("{name}/L"))?, jack.audio_out(&format!("{name}/R"))?, diff --git a/crates/tek/src/sampler/sampler_audio.rs b/crates/tek/src/sampler/sampler_audio.rs index 60efa41f..fac91ba6 100644 --- a/crates/tek/src/sampler/sampler_audio.rs +++ b/crates/tek/src/sampler/sampler_audio.rs @@ -1,15 +1,35 @@ use crate::*; -audio!(|self: SamplerTui, _client, scope|{ - self.state.process_midi_in(scope); - self.state.clear_output_buffer(); - self.state.process_audio_out(scope); - self.state.write_output_buffer(scope); +audio!(|self: SamplerTui, client, scope|{ + SamplerAudio(&mut self.state).process(client, scope) +}); + +pub struct SamplerAudio<'a>(pub &'a mut Sampler); + +audio!(|self: SamplerAudio<'a>, _client, scope|{ + self.0.process_midi_in(scope); + self.0.clear_output_buffer(); + self.0.process_audio_out(scope); + self.0.write_output_buffer(scope); + self.0.update_input_meter(scope); Control::Continue }); impl Sampler { + pub fn update_input_meter (&mut self, scope: &ProcessScope) { + let Sampler { audio_ins, input_meter, .. } = self; + if audio_ins.len() != input_meter.len() { + *input_meter = vec![0.0;audio_ins.len()]; + } + for (input, meter) in audio_ins.iter().zip(input_meter) { + let slice = input.as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + /// 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; diff --git a/crates/tek/src/status/status_groovebox.rs b/crates/tek/src/status/status_groovebox.rs index a00b30cd..a10b9b92 100644 --- a/crates/tek/src/status/status_groovebox.rs +++ b/crates/tek/src/status/status_groovebox.rs @@ -9,13 +9,13 @@ pub struct GrooveboxStatus { pub(crate) playing: bool, } from!(|state:&GrooveboxTui|GrooveboxStatus = { - let samples = state.clock.chunk.load(Relaxed); - let rate = state.clock.timebase.sr.get(); + let samples = state.clock().chunk.load(Relaxed); + let rate = state.clock().timebase.sr.get(); let buffer = samples as f64 / rate; let width = state.size.w(); Self { width, - playing: state.clock.is_rolling(), + playing: state.clock().is_rolling(), cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), }