From 0dec568fe4236b23849696968998908a07302629 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 14 Jul 2024 00:02:08 +0300 Subject: [PATCH] consolidate more; make help and setup into devices --- Justfile | 3 + src/config.rs | 52 ----- src/control.rs | 2 +- src/devices.rs | 2 +- src/{view => devices}/help.rs | 2 + src/devices/sampler.rs | 2 +- src/devices/setup.rs | 55 ++++++ src/model.rs | 347 +++++++++++++++++++++++++++++++++- src/model/axis.rs | 33 ---- src/model/phrase.rs | 93 --------- src/model/scene.rs | 14 -- src/model/track.rs | 201 -------------------- src/view.rs | 2 +- 13 files changed, 408 insertions(+), 400 deletions(-) rename src/{view => devices}/help.rs (99%) create mode 100644 src/devices/setup.rs delete mode 100644 src/model/axis.rs delete mode 100644 src/model/phrase.rs delete mode 100644 src/model/scene.rs delete mode 100644 src/model/track.rs diff --git a/Justfile b/Justfile index ac4de6d5..012ced10 100644 --- a/Justfile +++ b/Justfile @@ -7,3 +7,6 @@ status: push: git push -u codeberg main git push -u origin main +fpush: + git push -fu codeberg main + git push -fu origin main diff --git a/src/config.rs b/src/config.rs index 883eab99..4deff25c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -49,55 +49,3 @@ impl AppPaths { Ok(()) } } - -/// Appears on first run (i.e. if state dir is missing). -pub struct SetupModal(pub Option>, pub bool); - -render!(SetupModal |self, buf, area| { - for cell in buf.content.iter_mut() { - cell.fg = ratatui::style::Color::Gray; - cell.modifier = ratatui::style::Modifier::DIM; - } - let lines = [ - (" ", Style::default().white().on_black().not_dim().bold()), - (" Welcome to TEK! ", Style::default().white().on_black().not_dim().bold()), - (" ", Style::default().white().on_black().not_dim().bold()), - (" Press ENTER to create the ", Style::default().white().on_black().not_dim()), - (" following directories: ", Style::default().white().on_black().not_dim()), - (" ", Style::default().white().on_black().not_dim().bold()), - (" Configuration directory: ", Style::default().white().on_black().not_dim()), - (" ~/.config/tek ", Style::default().white().on_black().not_dim().bold()), - (" ", Style::default().white().on_black().not_dim()), - (" Data directory: ", Style::default().white().on_black().not_dim()), - (" ~/.local/share/tek ", Style::default().white().on_black().not_dim().bold()), - (" ", Style::default().white().on_black().not_dim().bold()), - (" Or press CTRL-C to exit. ", Style::default().white().on_black().not_dim()), - (" ", Style::default().white().on_black().not_dim()), - ]; - let width = lines[0].0.len() as u16; - let x = area.x + (area.width - width) / 2; - for (i, (line, style)) in lines.iter().enumerate() { - line.blit(buf, x, area.y + area.height / 2 - (lines.len() / 2) as u16 + i as u16, Some(*style))?; - } - Ok(area) -}); -handle!(SetupModal |self, e| { - if let AppEvent::Input(::crossterm::event::Event::Key(KeyEvent { - code: KeyCode::Enter, - .. - })) = e { - AppPaths::new(&self.0.as_ref().unwrap())?.create()?; - self.exit(); - Ok(true) - } else { - Ok(false) - } -}); -impl Exit for SetupModal { - fn exited (&self) -> bool { - self.1 - } - fn exit (&mut self) { - self.1 = true - } -} diff --git a/src/control.rs b/src/control.rs index c80bc48d..e9f96119 100644 --- a/src/control.rs +++ b/src/control.rs @@ -161,7 +161,7 @@ pub const KEYMAP_CHAIN: &'static [KeyBinding] = keymap!(App { /// Generic key bindings for views that support focus. pub const KEYMAP_FOCUS: &'static [KeyBinding] = keymap!(App { [Char(';'), NONE, "command", "open command palette", |app: &mut App| { - app.modal = Some(Box::new(crate::view::HelpModal::new())); + app.modal = Some(Box::new(crate::devices::help::HelpModal::new())); Ok(true) }], [Tab, NONE, "focus_next", "focus next area", focus_next], diff --git a/src/devices.rs b/src/devices.rs index f3b167f2..cf20ebe7 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -1,2 +1,2 @@ //! Music-making apparatuses. -crate::core::pubmod!{arranger looper mixer plugin sampler sequencer transport} +crate::core::pubmod!{arranger help looper mixer plugin sampler setup sequencer transport} diff --git a/src/view/help.rs b/src/devices/help.rs similarity index 99% rename from src/view/help.rs rename to src/devices/help.rs index f231cdf6..8d460bc6 100644 --- a/src/view/help.rs +++ b/src/devices/help.rs @@ -1,3 +1,5 @@ +//! Help modal / command palette. + use crate::{core::*, view::*}; /// Command palette. diff --git a/src/devices/sampler.rs b/src/devices/sampler.rs index ca34d460..c6c25539 100644 --- a/src/devices/sampler.rs +++ b/src/devices/sampler.rs @@ -1,6 +1,6 @@ //! Sampler (currently 16bit WAVs at system rate; TODO convert/resample) -use crate::{core::*, model::*}; +use crate::core::*; /// Key bindings for sampler device. pub const KEYMAP_SAMPLER: &'static [KeyBinding] = keymap!(Sampler { diff --git a/src/devices/setup.rs b/src/devices/setup.rs new file mode 100644 index 00000000..4e4ac8c4 --- /dev/null +++ b/src/devices/setup.rs @@ -0,0 +1,55 @@ +//! Inital setup dialog (TODO: make this the options dialog too?) + +use crate::{core::*, config::AppPaths}; + +/// Appears on first run (i.e. if state dir is missing). +pub struct SetupModal(pub Option>, pub bool); + +render!(SetupModal |self, buf, area| { + for cell in buf.content.iter_mut() { + cell.fg = ratatui::style::Color::Gray; + cell.modifier = ratatui::style::Modifier::DIM; + } + let lines = [ + (" ", Style::default().white().on_black().not_dim().bold()), + (" Welcome to TEK! ", Style::default().white().on_black().not_dim().bold()), + (" ", Style::default().white().on_black().not_dim().bold()), + (" Press ENTER to create the ", Style::default().white().on_black().not_dim()), + (" following directories: ", Style::default().white().on_black().not_dim()), + (" ", Style::default().white().on_black().not_dim().bold()), + (" Configuration directory: ", Style::default().white().on_black().not_dim()), + (" ~/.config/tek ", Style::default().white().on_black().not_dim().bold()), + (" ", Style::default().white().on_black().not_dim()), + (" Data directory: ", Style::default().white().on_black().not_dim()), + (" ~/.local/share/tek ", Style::default().white().on_black().not_dim().bold()), + (" ", Style::default().white().on_black().not_dim().bold()), + (" Or press CTRL-C to exit. ", Style::default().white().on_black().not_dim()), + (" ", Style::default().white().on_black().not_dim()), + ]; + let width = lines[0].0.len() as u16; + let x = area.x + (area.width - width) / 2; + for (i, (line, style)) in lines.iter().enumerate() { + line.blit(buf, x, area.y + area.height / 2 - (lines.len() / 2) as u16 + i as u16, Some(*style))?; + } + Ok(area) +}); +handle!(SetupModal |self, e| { + if let AppEvent::Input(::crossterm::event::Event::Key(KeyEvent { + code: KeyCode::Enter, + .. + })) = e { + AppPaths::new(&self.0.as_ref().unwrap())?.create()?; + self.exit(); + Ok(true) + } else { + Ok(false) + } +}); +impl Exit for SetupModal { + fn exited (&self) -> bool { + self.1 + } + fn exit (&mut self) { + self.1 = true + } +} diff --git a/src/model.rs b/src/model.rs index c77f3482..757e4225 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,8 +2,6 @@ use crate::{core::*, devices::{arranger::*, sequencer::*, transport::*}}; -submod! { axis phrase scene track } - /// Root of application state. pub struct App { /// Optional modal dialog @@ -42,7 +40,7 @@ impl App { let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0); Ok(Self { modal: first_run.then(||{ - Exit::boxed(crate::config::SetupModal(Some(xdg.clone()), false)) + Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false)) }), entered: true, @@ -60,6 +58,7 @@ impl App { }) } } + process!(App |self, _client, scope| { let ( reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs @@ -81,6 +80,7 @@ process!(App |self, _client, scope| { } Control::Continue }); + impl App { pub fn client (&self) -> &Client { self.jack.as_ref().unwrap().client() @@ -154,3 +154,344 @@ impl AppFocus { } } } + +/// A sequencer track. +pub struct Track { + pub name: String, + /// Play input through output. + pub monitoring: bool, + /// Write input to sequence. + pub recording: bool, + /// Overdub input to sequence. + pub overdub: bool, + /// Map: tick -> MIDI events at tick + pub phrases: Vec>>, + /// Phrase selector + pub sequence: Option, + /// Output from current sequence. + pub midi_out: Option>, + /// MIDI output buffer + midi_out_buf: Vec>>, + /// Device chain + pub devices: Vec, + /// Device selector + pub device: usize, + /// Send all notes off + pub reset: bool, // TODO?: after Some(nframes) + /// Highlight keys on piano roll. + pub notes_in: [bool;128], + /// Highlight keys on piano roll. + pub notes_out: [bool;128], +} + +impl Track { + pub fn new (name: &str) -> Usually { + Ok(Self { + name: name.to_string(), + midi_out: None, + midi_out_buf: vec![Vec::with_capacity(16);16384], + notes_in: [false;128], + notes_out: [false;128], + monitoring: false, + recording: false, + overdub: true, + sequence: None, + phrases: vec![], + devices: vec![], + device: 0, + reset: true, + }) + } + fn get_device_mut (&self, i: usize) -> Option>> { + self.devices.get(i).map(|d|d.state.write().unwrap()) + } + pub fn device_mut (&self) -> Option>> { + self.get_device_mut(self.device) + } + pub fn connect_first_device (&self) -> Usually<()> { + if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) { + device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?; + } + Ok(()) + } + pub fn connect_last_device (&self, app: &App) -> Usually<()> { + Ok(match self.devices.get(self.devices.len().saturating_sub(1)) { + Some(device) => { + app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?; + app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?; + () + }, + None => () + }) + } + pub fn add_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> { + self.devices.push(device); + let index = self.devices.len() - 1; + Ok(&mut self.devices[index]) + } + pub fn toggle_monitor (&mut self) { + self.monitoring = !self.monitoring; + } + pub fn toggle_record (&mut self) { + self.recording = !self.recording; + } + pub fn toggle_overdub (&mut self) { + self.overdub = !self.overdub; + } + pub fn process ( + &mut self, + input: Option, + timebase: &Arc, + playing: Option, + started: Option<(usize, usize)>, + quant: usize, + reset: bool, + scope: &ProcessScope, + (frame0, frames): (usize, usize), + (_usec0, _usecs): (usize, usize), + period: f64, + ) { + if self.midi_out.is_some() { + // Clear the section of the output buffer that we will be using + for frame in &mut self.midi_out_buf[0..frames] { + frame.clear(); + } + // Emit "all notes off" at start of buffer if requested + if self.reset { + all_notes_off(&mut self.midi_out_buf); + self.reset = false; + } else if reset { + all_notes_off(&mut self.midi_out_buf); + } + } + if let ( + Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase) + ) = ( + playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id)) + ) { + phrase.read().map(|phrase|{ + if self.midi_out.is_some() { + phrase.process_out( + &mut self.midi_out_buf, + &mut self.notes_out, + timebase, + (frame0.saturating_sub(start_frame), frames, period) + ); + } + }).unwrap(); + let mut phrase = phrase.write().unwrap(); + let length = phrase.length; + // Monitor and record input + if input.is_some() && (self.recording || self.monitoring) { + // For highlighting keys and note repeat + for (frame, event, bytes) in parse_midi_input(input.unwrap()) { + match event { + LiveEvent::Midi { message, .. } => { + if self.monitoring { + self.midi_out_buf[frame].push(bytes.to_vec()) + } + if self.recording { + phrase.record_event({ + let pulse = timebase.frame_to_pulse( + (frame0 + frame - start_frame) as f64 + ); + let quantized = ( + pulse / quant as f64 + ).round() as usize * quant; + let looped = quantized % length; + looped + }, message); + } + match message { + MidiMessage::NoteOn { key, .. } => { + self.notes_in[key.as_int() as usize] = true; + } + MidiMessage::NoteOff { key, .. } => { + self.notes_in[key.as_int() as usize] = false; + }, + _ => {} + } + }, + _ => {} + } + } + } + } else if input.is_some() && self.midi_out.is_some() && self.monitoring { + for (frame, event, bytes) in parse_midi_input(input.unwrap()) { + self.process_monitor_event(frame, &event, bytes) + } + } + if let Some(out) = &mut self.midi_out { + write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames); + } + } + + #[inline] + fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) { + match event { + LiveEvent::Midi { message, .. } => { + self.write_to_output_buffer(frame, bytes); + self.process_monitor_message(&message); + }, + _ => {} + } + } + + #[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) { + self.midi_out_buf[frame].push(bytes.to_vec()); + } + + #[inline] + fn process_monitor_message (&mut self, message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { + self.notes_in[key.as_int() as usize] = true; + } + MidiMessage::NoteOff { key, .. } => { + self.notes_in[key.as_int() as usize] = false; + }, + _ => {} + } + } +} + +/// Define a MIDI phrase. +#[macro_export] macro_rules! phrase { + ($($t:expr => $msg:expr),* $(,)?) => {{ + #[allow(unused_mut)] + let mut phrase = BTreeMap::new(); + $(phrase.insert($t, vec![]);)* + $(phrase.get_mut(&$t).unwrap().push($msg);)* + phrase + }} +} + +pub type PhraseData = Vec>; + +#[derive(Debug)] +/// A MIDI sequence. +pub struct Phrase { + pub name: String, + pub length: usize, + pub notes: PhraseData, + pub looped: Option<(usize, usize)>, + /// Immediate note-offs in view + pub percussive: bool +} + +impl Default for Phrase { + fn default () -> Self { + Self::new("", 0, None) + } +} + +impl Phrase { + pub fn new (name: &str, length: usize, notes: Option) -> Self { + Self { + name: name.to_string(), + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped: Some((0, length)), + percussive: true, + } + } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { + panic!("extend phrase 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 { + //panic!("{:?} {start} {end}", &self); + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + match event { + MidiMessage::NoteOn {key,..} => { + if *key == k { + return true + } + } + _ => {} + } + } + } + return false + } + /// Write a chunk of MIDI events to an output port. + pub fn process_out ( + &self, + output: &mut MIDIChunk, + notes_on: &mut [bool;128], + timebase: &Arc, + (frame0, frames, _): (usize, usize, f64), + ) { + let mut buf = Vec::with_capacity(8); + for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames( + frame0, frame0 + frames + ) { + let tick = tick % self.length; + for message in self.notes[tick].iter() { + buf.clear(); + let channel = 0.into(); + let message = *message; + LiveEvent::Midi { channel, message }.write(&mut buf).unwrap(); + output[time as usize].push(buf.clone()); + match message { + MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true, + MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false, + _ => {} + } + } + } + } +} + +/// A collection of phrases to play on each track. +pub struct Scene { + pub name: String, + pub clips: Vec>, +} + +impl Scene { + pub fn new (name: impl AsRef, clips: impl AsRef<[Option]>) -> Self { + Self { + name: name.as_ref().into(), + clips: clips.as_ref().iter().map(|x|x.clone()).collect() + } + } +} + +macro_rules! impl_axis_common { ($A:ident $T:ty) => { + impl $A<$T> { + pub fn start_inc (&mut self) -> $T { + self.start = self.start + 1; + self.start + } + pub fn start_dec (&mut self) -> $T { + self.start = self.start.saturating_sub(1); + self.start + } + pub fn point_inc (&mut self) -> Option<$T> { + self.point = self.point.map(|p|p + 1); + self.point + } + pub fn point_dec (&mut self) -> Option<$T> { + self.point = self.point.map(|p|p.saturating_sub(1)); + self.point + } + } +} } + +pub struct FixedAxis { pub start: T, pub point: Option } +impl_axis_common!(FixedAxis u16); +impl_axis_common!(FixedAxis usize); + +pub struct ScaledAxis { pub start: T, pub scale: T, pub point: Option } +impl_axis_common!(ScaledAxis u16); +impl_axis_common!(ScaledAxis usize); +impl ScaledAxis { + pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) { + self.scale = cb(self.scale) + } +} diff --git a/src/model/axis.rs b/src/model/axis.rs deleted file mode 100644 index 2a66be00..00000000 --- a/src/model/axis.rs +++ /dev/null @@ -1,33 +0,0 @@ -macro_rules! impl_axis_common { ($A:ident $T:ty) => { - impl $A<$T> { - pub fn start_inc (&mut self) -> $T { - self.start = self.start + 1; - self.start - } - pub fn start_dec (&mut self) -> $T { - self.start = self.start.saturating_sub(1); - self.start - } - pub fn point_inc (&mut self) -> Option<$T> { - self.point = self.point.map(|p|p + 1); - self.point - } - pub fn point_dec (&mut self) -> Option<$T> { - self.point = self.point.map(|p|p.saturating_sub(1)); - self.point - } - } -} } - -pub struct FixedAxis { pub start: T, pub point: Option } -impl_axis_common!(FixedAxis u16); -impl_axis_common!(FixedAxis usize); - -pub struct ScaledAxis { pub start: T, pub scale: T, pub point: Option } -impl_axis_common!(ScaledAxis u16); -impl_axis_common!(ScaledAxis usize); -impl ScaledAxis { - pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) { - self.scale = cb(self.scale) - } -} diff --git a/src/model/phrase.rs b/src/model/phrase.rs deleted file mode 100644 index f9a06299..00000000 --- a/src/model/phrase.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::core::*; - -/// Define a MIDI phrase. -#[macro_export] macro_rules! phrase { - ($($t:expr => $msg:expr),* $(,)?) => {{ - #[allow(unused_mut)] - let mut phrase = BTreeMap::new(); - $(phrase.insert($t, vec![]);)* - $(phrase.get_mut(&$t).unwrap().push($msg);)* - phrase - }} -} - -pub type PhraseData = Vec>; - -#[derive(Debug)] -/// A MIDI sequence. -pub struct Phrase { - pub name: String, - pub length: usize, - pub notes: PhraseData, - pub looped: Option<(usize, usize)>, - /// Immediate note-offs in view - pub percussive: bool -} - -impl Default for Phrase { - fn default () -> Self { - Self::new("", 0, None) - } -} - -impl Phrase { - pub fn new (name: &str, length: usize, notes: Option) -> Self { - Self { - name: name.to_string(), - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped: Some((0, length)), - percussive: true, - } - } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { - panic!("extend phrase 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 { - //panic!("{:?} {start} {end}", &self); - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - match event { - MidiMessage::NoteOn {key,..} => { - if *key == k { - return true - } - } - _ => {} - } - } - } - return false - } - /// Write a chunk of MIDI events to an output port. - pub fn process_out ( - &self, - output: &mut MIDIChunk, - notes_on: &mut [bool;128], - timebase: &Arc, - (frame0, frames, _): (usize, usize, f64), - ) { - let mut buf = Vec::with_capacity(8); - for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames( - frame0, frame0 + frames - ) { - let tick = tick % self.length; - for message in self.notes[tick].iter() { - buf.clear(); - let channel = 0.into(); - let message = *message; - LiveEvent::Midi { channel, message }.write(&mut buf).unwrap(); - output[time as usize].push(buf.clone()); - match message { - MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true, - MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false, - _ => {} - } - } - } - } -} diff --git a/src/model/scene.rs b/src/model/scene.rs deleted file mode 100644 index 7c23249a..00000000 --- a/src/model/scene.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// A collection of phrases to play on each track. -pub struct Scene { - pub name: String, - pub clips: Vec>, -} - -impl Scene { - pub fn new (name: impl AsRef, clips: impl AsRef<[Option]>) -> Self { - Self { - name: name.as_ref().into(), - clips: clips.as_ref().iter().map(|x|x.clone()).collect() - } - } -} diff --git a/src/model/track.rs b/src/model/track.rs deleted file mode 100644 index 4eb7153a..00000000 --- a/src/model/track.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::{core::*, model::*}; - -/// A sequencer track. -pub struct Track { - pub name: String, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Overdub input to sequence. - pub overdub: bool, - /// Map: tick -> MIDI events at tick - pub phrases: Vec>>, - /// Phrase selector - pub sequence: Option, - /// Output from current sequence. - pub midi_out: Option>, - /// MIDI output buffer - midi_out_buf: Vec>>, - /// Device chain - pub devices: Vec, - /// Device selector - pub device: usize, - /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) - /// Highlight keys on piano roll. - pub notes_in: [bool;128], - /// Highlight keys on piano roll. - pub notes_out: [bool;128], -} - -impl Track { - pub fn new (name: &str) -> Usually { - Ok(Self { - name: name.to_string(), - midi_out: None, - midi_out_buf: vec![Vec::with_capacity(16);16384], - notes_in: [false;128], - notes_out: [false;128], - monitoring: false, - recording: false, - overdub: true, - sequence: None, - phrases: vec![], - devices: vec![], - device: 0, - reset: true, - }) - } - fn get_device_mut (&self, i: usize) -> Option>> { - self.devices.get(i).map(|d|d.state.write().unwrap()) - } - pub fn device_mut (&self) -> Option>> { - self.get_device_mut(self.device) - } - pub fn connect_first_device (&self) -> Usually<()> { - if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) { - device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?; - } - Ok(()) - } - pub fn connect_last_device (&self, app: &App) -> Usually<()> { - Ok(match self.devices.get(self.devices.len().saturating_sub(1)) { - Some(device) => { - app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?; - app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?; - () - }, - None => () - }) - } - pub fn add_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> { - self.devices.push(device); - let index = self.devices.len() - 1; - Ok(&mut self.devices[index]) - } - pub fn toggle_monitor (&mut self) { - self.monitoring = !self.monitoring; - } - pub fn toggle_record (&mut self) { - self.recording = !self.recording; - } - pub fn toggle_overdub (&mut self) { - self.overdub = !self.overdub; - } - pub fn process ( - &mut self, - input: Option, - timebase: &Arc, - playing: Option, - started: Option<(usize, usize)>, - quant: usize, - reset: bool, - scope: &ProcessScope, - (frame0, frames): (usize, usize), - (_usec0, _usecs): (usize, usize), - period: f64, - ) { - if self.midi_out.is_some() { - // Clear the section of the output buffer that we will be using - for frame in &mut self.midi_out_buf[0..frames] { - frame.clear(); - } - // Emit "all notes off" at start of buffer if requested - if self.reset { - all_notes_off(&mut self.midi_out_buf); - self.reset = false; - } else if reset { - all_notes_off(&mut self.midi_out_buf); - } - } - if let ( - Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase) - ) = ( - playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id)) - ) { - phrase.read().map(|phrase|{ - if self.midi_out.is_some() { - phrase.process_out( - &mut self.midi_out_buf, - &mut self.notes_out, - timebase, - (frame0.saturating_sub(start_frame), frames, period) - ); - } - }).unwrap(); - let mut phrase = phrase.write().unwrap(); - let length = phrase.length; - // Monitor and record input - if input.is_some() && (self.recording || self.monitoring) { - // For highlighting keys and note repeat - for (frame, event, bytes) in parse_midi_input(input.unwrap()) { - match event { - LiveEvent::Midi { message, .. } => { - if self.monitoring { - self.midi_out_buf[frame].push(bytes.to_vec()) - } - if self.recording { - phrase.record_event({ - let pulse = timebase.frame_to_pulse( - (frame0 + frame - start_frame) as f64 - ); - let quantized = ( - pulse / quant as f64 - ).round() as usize * quant; - let looped = quantized % length; - looped - }, message); - } - match message { - MidiMessage::NoteOn { key, .. } => { - self.notes_in[key.as_int() as usize] = true; - } - MidiMessage::NoteOff { key, .. } => { - self.notes_in[key.as_int() as usize] = false; - }, - _ => {} - } - }, - _ => {} - } - } - } - } else if input.is_some() && self.midi_out.is_some() && self.monitoring { - for (frame, event, bytes) in parse_midi_input(input.unwrap()) { - self.process_monitor_event(frame, &event, bytes) - } - } - if let Some(out) = &mut self.midi_out { - write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames); - } - } - - #[inline] - fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) { - match event { - LiveEvent::Midi { message, .. } => { - self.write_to_output_buffer(frame, bytes); - self.process_monitor_message(&message); - }, - _ => {} - } - } - - #[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) { - self.midi_out_buf[frame].push(bytes.to_vec()); - } - - #[inline] - fn process_monitor_message (&mut self, message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { - self.notes_in[key.as_int() as usize] = true; - } - MidiMessage::NoteOff { key, .. } => { - self.notes_in[key.as_int() as usize] = false; - }, - _ => {} - } - } -} diff --git a/src/view.rs b/src/view.rs index ad6e7231..e1fb3cc3 100644 --- a/src/view.rs +++ b/src/view.rs @@ -2,7 +2,7 @@ use crate::{render, App, core::*}; -submod! { border chain help split theme } +submod! { border chain split theme } render!(App |self, buf, area| { Split::down([