diff --git a/demos/project.edn b/demos/project.edn index 02b601f7..f21b3940 100644 --- a/demos/project.edn +++ b/demos/project.edn @@ -20,7 +20,7 @@ (:12 (36 100)) (:14 (36 110))) (phrase { :name "D Beat" :beats 4 :steps 16 } - (:00 (36 128)) + (:00 (36 128) (49 110)) (:00 (44 50)) (:02 (44 30)) (:04 (40 100)) @@ -89,10 +89,16 @@ (sample { :midi 34 :name "808" :file "808.wav" }) (sample { :midi 35 :name "KC1" :file "kik.wav" }) (sample { :midi 36 :name "KC2" :file "kik2.wav" }) + (sample { :midi 37 :name "RIM" :file "rim.wav" }) (sample { :midi 38 :name "SN1" :file "sna.wav" }) + (sample { :midi 39 :name "SHK" :file "shk.wav" }) (sample { :midi 40 :name "SN2" :file "sna2.wav" }) (sample { :midi 42 :name "HH1" :file "chh.wav" }) - (sample { :midi 44 :name "HH2" :file "chh2.wav" }))) + (sample { :midi 45 :name "HH1" :file "ohh.wav" }) + (sample { :midi 46 :name "HH1" :file "ohh1.wav" }) + (sample { :midi 47 :name "HH1" :file "ohh2.wav" }) + (sample { :midi 44 :name "HH2" :file "chh2.wav" }) + (sample { :midi 49 :name "CRS" :file "crs.wav" }))) (track { :name "Bass" :gain +0.0 } (phrase { :name "Bass 1" :beats 4 }) diff --git a/src/control.rs b/src/control.rs index 632aec14..e2eaefbc 100644 --- a/src/control.rs +++ b/src/control.rs @@ -96,11 +96,11 @@ const KEYMAP: &'static [KeyBinding] = keymap!(App { }], [Char('='), NONE, "zoom_in", "Zoom in", |app: &mut App| { - app.time_zoom = prev_note_length(app.time_zoom); + app.seq_buf.time_zoom = prev_note_length(app.seq_buf.time_zoom); Ok(true) }], [Char('-'), NONE, "zoom_out", "Zoom out", |app: &mut App| { - app.time_zoom = next_note_length(app.time_zoom); + app.seq_buf.time_zoom = next_note_length(app.seq_buf.time_zoom); Ok(true) }], diff --git a/src/control/arranger.rs b/src/control/arranger.rs index 659b0422..08f7caa4 100644 --- a/src/control/arranger.rs +++ b/src/control/arranger.rs @@ -1,28 +1,32 @@ use crate::{core::*, model::App}; pub const KEYMAP_ARRANGER: &'static [KeyBinding] = keymap!(App { + [Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut App| { + app.arranger_mode = !app.seq_mode; + Ok(true) + }], [Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut App| Ok( match app.arranger_mode { - false => {app.scene_cursor = app.scene_cursor.saturating_sub(1); true}, - true => {app.track_cursor = app.track_cursor.saturating_sub(1); true}, + false => {app.prev_scene();true}, + true => {app.prev_track();true}, } )], [Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut App| Ok( match app.arranger_mode { - false => {app.scene_cursor = app.scenes.len().min(app.scene_cursor + 1); true}, - true => {app.track_cursor = app.tracks.len().min(app.track_cursor + 1); true}, + false => {app.next_scene();true}, + true => {app.next_track();true}, } )], [Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut App| Ok( match app.arranger_mode { - false => {app.track_cursor = app.track_cursor.saturating_sub(1); true}, - true => {app.scene_cursor = app.scene_cursor.saturating_sub(1); true}, + false => {app.prev_track();true}, + true => {app.prev_scene();true}, } )], [Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut App| Ok( match app.arranger_mode { - false => {app.track_cursor = app.tracks.len().min(app.track_cursor + 1); true}, - true => {app.scene_cursor = app.scenes.len().min(app.scene_cursor + 1); true}, + false => {app.next_track();true}, + true => {app.next_scene();true} } )], [Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut App| Ok( @@ -44,39 +48,11 @@ pub const KEYMAP_ARRANGER: &'static [KeyBinding] = keymap!(App { } )], [Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut App| { - //if state.cursor.0 >= 1 && state.cursor.1 >= 1 { - //let scene_id = state.cursor.1 - 1; - //let clip_id = state.cursor.0 - 1; - //let scene = &mut state.scenes[scene_id]; - //scene.clips[clip_id] = match scene.clips[clip_id] { - //None => Some(0), - //Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) { - //None - //} else { - //Some(i + 1) - //} - //}; - //} + app.next_phrase(); Ok(true) }], [Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut App| { - //if state.cursor.0 >= 1 && state.cursor.1 >= 1 { - //let scene_id = state.cursor.1 - 1; - //let clip_id = state.cursor.0 - 1; - //let scene = &mut state.scenes[scene_id]; - //scene.clips[clip_id] = match scene.clips[clip_id] { - //None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)), - //Some(i) => if i == 0 { - //None - //} else { - //Some(i - 1) - //} - //}; - //} - Ok(true) - }], - [Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut App| { - app.arranger_mode = !app.seq_mode; + app.prev_phrase(); Ok(true) }], }); diff --git a/src/control/sequencer.rs b/src/control/sequencer.rs index 3fd5d4e5..73a309c0 100644 --- a/src/control/sequencer.rs +++ b/src/control/sequencer.rs @@ -13,7 +13,7 @@ pub const KEYMAP_SEQUENCER: &'static [KeyBinding] = keymap!(App { if app.entered { app.time_cursor = app.time_cursor.saturating_sub(1); } else { - app.time_start = app.time_start.saturating_sub(1); + app.seq_buf.time_start = app.seq_buf.time_start.saturating_sub(1); } Ok(true) }], @@ -21,7 +21,7 @@ pub const KEYMAP_SEQUENCER: &'static [KeyBinding] = keymap!(App { if app.entered { app.time_cursor = app.time_cursor + 1; } else { - app.time_start = app.time_start + 1; + app.seq_buf.time_start = app.seq_buf.time_start + 1; } Ok(true) }], diff --git a/src/model.rs b/src/model.rs index 9bf14953..6289b68c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -13,7 +13,7 @@ pub use self::sampler::{Sampler, Sample, read_sample_data}; pub use self::mixer::Mixer; pub use self::plugin::{Plugin, PluginKind, lv2::LV2Plugin}; -use crate::core::*; +use crate::{core::*, view::*}; pub struct App { /// Paths to user directories @@ -43,6 +43,8 @@ pub struct App { pub chain_mode: bool, /// Display mode of sequencer seciton pub seq_mode: bool, + /// Display buffer for sequencer + pub seq_buf: BufferedSequencerView, /// Optional modal dialog pub modal: Option>, /// Currently focused section @@ -57,10 +59,6 @@ pub struct App { pub note_start: usize, /// Display position of cursor within time range pub time_cursor: usize, - /// PPQ per display unit - pub time_zoom: usize, - /// Range of time steps to display - pub time_start: usize, /// Focused scene+1, 0 is track list pub scene_cursor: usize, /// Collection of scenes @@ -105,9 +103,8 @@ impl App { scenes: vec![], section: AppSection::default(), seq_mode: false, + seq_buf: BufferedSequencerView::new(96, 16384), time_cursor: 0, - time_start: 0, - time_zoom: 12, timebase: Arc::new(Timebase::default()), track_cursor: 1, tracks: vec![], diff --git a/src/model/phrase.rs b/src/model/phrase.rs index 3480b66a..a84a3f7b 100644 --- a/src/model/phrase.rs +++ b/src/model/phrase.rs @@ -1,5 +1,56 @@ use crate::{core::*, model::App}; +impl App { + pub fn next_phrase (&mut self) { + if let Some((track_index, track)) = self.track_mut() { + let phrases = track.phrases.len(); + if let Some((_, scene)) = self.scene_mut() { + if let Some(phrase_index) = scene.clips[track_index] { + if phrase_index >= phrases - 1 { + scene.clips[track_index] = None; + } else { + scene.clips[track_index] = Some(phrase_index + 1); + } + } else if phrases > 0 { + scene.clips[track_index] = Some(0); + } + } + } + } + pub fn prev_phrase (&mut self) { + if let Some((track_index, track)) = self.track_mut() { + let phrases = track.phrases.len(); + if let Some((_, scene)) = self.scene_mut() { + if let Some(phrase_index) = scene.clips[track_index] { + scene.clips[track_index] = if phrase_index == 0 { + None + } else { + Some(phrase_index - 1) + }; + } else if phrases > 0 { + scene.clips[track_index] = Some(phrases - 1); + } + } + } + } + pub fn phrase (&self) -> Option<&Phrase> { + let (track_id, track) = self.track()?; + let (_, scene) = self.scene()?; + track.phrases.get((*scene.clips.get(track_id)?)?) + } + pub fn phrase_mut (&mut self) -> Option<&mut Phrase> { + let (track_id, _) = self.track()?; + let (_, scene) = self.scene()?; + let clip = (*scene.clips.get(track_id)?)?; + self.track_mut()?.1.phrases.get_mut(clip) + } + fn phrase_id (&self) -> Option { + let (track_id, _) = self.track()?; + let (_, scene) = self.scene()?; + *scene.clips.get(track_id)? + } +} + /// Define a MIDI phrase. #[macro_export] macro_rules! phrase { ($($t:expr => $msg:expr),* $(,)?) => {{ @@ -87,22 +138,3 @@ impl Phrase { } } } - -impl App { - pub fn phrase (&self) -> Option<&Phrase> { - let (track_id, track) = self.track()?; - let (_, scene) = self.scene()?; - track.phrases.get((*scene.clips.get(track_id)?)?) - } - pub fn phrase_mut (&mut self) -> Option<&mut Phrase> { - let (track_id, _) = self.track()?; - let (_, scene) = self.scene()?; - let clip = (*scene.clips.get(track_id)?)?; - self.track_mut()?.1.phrases.get_mut(clip) - } - pub fn phrase_id (&self) -> Option { - let (track_id, _) = self.track()?; - let (_, scene) = self.scene()?; - *scene.clips.get(track_id)? - } -} diff --git a/src/model/scene.rs b/src/model/scene.rs index 9f194aca..52b1e281 100644 --- a/src/model/scene.rs +++ b/src/model/scene.rs @@ -1,21 +1,13 @@ use crate::{core::*, model::App}; -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() - } - } -} - impl App { - pub fn new_scene_name (&self) -> String { + pub fn next_scene (&mut self) { + self.scene_cursor = self.scenes.len().min(self.scene_cursor + 1); + } + pub fn prev_scene (&mut self) { + self.scene_cursor = self.scene_cursor.saturating_sub(1); + } + fn new_scene_name (&self) -> String { format!("Scene {}", self.scenes.len() + 1) } pub fn add_scene (&mut self, name: Option<&str>) -> Usually<&mut Scene> { @@ -47,3 +39,17 @@ impl App { } } } } + +pub struct Scene { + pub name: String, + pub clips: Vec>, +} + +impl Scene { + 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 index 68bd8a9d..7df0deba 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -1,6 +1,52 @@ use crate::core::*; use crate::model::*; +impl App { + pub fn next_track (&mut self) { + self.track_cursor = self.tracks.len().min(self.track_cursor + 1); + } + pub fn prev_track (&mut self) { + self.track_cursor = self.track_cursor.saturating_sub(1); + } + + pub fn new_track_name (&self) -> String { + format!("Track {}", self.tracks.len() + 1) + } + + pub fn add_track (&mut self, name: Option<&str>) -> Usually<&mut Track> { + let name = name.ok_or_else(||self.new_track_name())?; + self.tracks.push(Track::new(&name, None, None)?); + self.track_cursor = self.tracks.len(); + Ok(&mut self.tracks[self.track_cursor - 1]) + } + + fn add_track_with_cb ( + &mut self, name: Option<&str>, init: impl FnOnce(&Client, &mut Track)->Usually<()>, + ) -> Usually<&mut Track> { + let name = name.ok_or_else(||self.new_track_name())?; + let mut track = Track::new(&name, None, None)?; + init(self.client(), &mut track)?; + self.tracks.push(track); + self.track_cursor = self.tracks.len(); + Ok(&mut self.tracks[self.track_cursor - 1]) + } + + pub fn track (&self) -> Option<(usize, &Track)> { + match self.track_cursor { 0 => None, _ => { + let id = self.track_cursor as usize - 1; + self.tracks.get(id).map(|t|(id, t)) + } } + } + + pub fn track_mut (&mut self) -> Option<(usize, &mut Track)> { + match self.track_cursor { 0 => None, _ => { + let id = self.track_cursor as usize - 1; + self.tracks.get_mut(id).map(|t|(id, t)) + } } + } + +} + pub struct Track { pub name: String, /// Play input through output. @@ -29,7 +75,7 @@ pub struct Track { pub notes_out: [bool;128], } impl Track { - pub fn new ( + fn new ( name: &str, phrases: Option>, devices: Option>, @@ -50,19 +96,19 @@ impl Track { reset: true, }) } - pub fn get_device (&self, i: usize) -> Option>> { + fn get_device (&self, i: usize) -> Option>> { self.devices.get(i).map(|d|d.state.read().unwrap()) } - pub fn get_device_mut (&self, i: usize) -> Option>> { + fn get_device_mut (&self, i: usize) -> Option>> { self.devices.get(i).map(|d|d.state.write().unwrap()) } - pub fn device (&self) -> Option>> { + fn device (&self) -> Option>> { self.get_device(self.device) } pub fn device_mut (&self) -> Option>> { self.get_device_mut(self.device) } - pub fn first_device (&self) -> Option>> { + fn first_device (&self) -> Option>> { self.get_device(0) } pub fn connect_first_device (&self) -> Usually<()> { @@ -71,7 +117,7 @@ impl Track { } Ok(()) } - pub fn last_device (&self) -> Option>> { + fn last_device (&self) -> Option>> { self.get_device(self.devices.len().saturating_sub(1)) } pub fn connect_last_device (&self, app: &App) -> Usually<()> { @@ -89,7 +135,7 @@ impl Track { let index = self.devices.len() - 1; Ok(&mut self.devices[index]) } - pub fn add_device_with_cb ( + fn add_device_with_cb ( &mut self, mut device: JackDevice, init: impl Fn(&Self, &mut JackDevice)->Usually<()> @@ -99,21 +145,21 @@ impl Track { let index = self.devices.len() - 1; Ok(&mut self.devices[index]) } - pub fn phrase (&self) -> Option<&Phrase> { + fn phrase (&self) -> Option<&Phrase> { if let Some(phrase) = self.sequence { return self.phrases.get(phrase) } else { None } } - pub fn phrase_mut (&mut self) -> Option<&mut Phrase> { + fn phrase_mut (&mut self) -> Option<&mut Phrase> { if let Some(phrase) = self.sequence { return self.phrases.get_mut(phrase) } else { None } } - pub fn add_phrase ( + fn add_phrase ( &mut self, name: &str, length: usize, data: Option ) -> &mut Phrase { self.phrases.push(Phrase::new(name, length, data)); @@ -243,43 +289,3 @@ impl Track { } } } - -impl App { - - pub fn new_track_name (&self) -> String { - format!("Track {}", self.tracks.len() + 1) - } - - pub fn add_track (&mut self, name: Option<&str>) -> Usually<&mut Track> { - let name = name.ok_or_else(||self.new_track_name())?; - self.tracks.push(Track::new(&name, None, None)?); - self.track_cursor = self.tracks.len(); - Ok(&mut self.tracks[self.track_cursor - 1]) - } - - pub fn add_track_with_cb ( - &mut self, name: Option<&str>, init: impl FnOnce(&Client, &mut Track)->Usually<()>, - ) -> Usually<&mut Track> { - let name = name.ok_or_else(||self.new_track_name())?; - let mut track = Track::new(&name, None, None)?; - init(self.client(), &mut track)?; - self.tracks.push(track); - self.track_cursor = self.tracks.len(); - Ok(&mut self.tracks[self.track_cursor - 1]) - } - - pub fn track (&self) -> Option<(usize, &Track)> { - match self.track_cursor { 0 => None, _ => { - let id = self.track_cursor as usize - 1; - self.tracks.get(id).map(|t|(id, t)) - } } - } - - pub fn track_mut (&mut self) -> Option<(usize, &mut Track)> { - match self.track_cursor { 0 => None, _ => { - let id = self.track_cursor as usize - 1; - self.tracks.get_mut(id).map(|t|(id, t)) - } } - } - -} diff --git a/src/view.rs b/src/view.rs index 6fd2cb2e..ba8c04fe 100644 --- a/src/view.rs +++ b/src/view.rs @@ -12,7 +12,7 @@ pub use self::layout::*; pub use self::transport::TransportView; pub use self::arranger::*; pub use self::chain::ChainView; -pub use self::sequencer::SequencerView; +pub use self::sequencer::{SequencerView, BufferedSequencerView}; use crate::{render, App, core::*}; @@ -22,7 +22,7 @@ render!(App |self, buf, area| { &ArrangerView::new(&self, !self.arranger_mode), &If(self.track_cursor > 0, &Split::right([ &ChainView::vertical(&self), - &SequencerView::new(&self), + &self.seq_buf, ])) ]).render(buf, area)?; if let Some(ref modal) = self.modal { diff --git a/src/view/plugin.rs b/src/view/plugin.rs index b704fa91..d79c2168 100644 --- a/src/view/plugin.rs +++ b/src/view/plugin.rs @@ -24,7 +24,7 @@ pub fn render (state: &Plugin, buf: &mut Buffer, area: Rect) Some(Style::default().green()) } else { None - }); + })?; } else { break } @@ -39,10 +39,10 @@ pub fn render (state: &Plugin, buf: &mut Buffer, area: Rect) fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usually { let style = Style::default().gray(); let label1 = format!(" {}", state.name); - label1.blit(buf, x + 1, y, Some(style.white().bold())); + label1.blit(buf, 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()))]); - label2.blit(buf, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + label2.blit(buf, x + 2 + label1.len() as u16, y, Some(style.not_dim()))?; } Ok(Rect { x, y, width: w, height: 1 }) } diff --git a/src/view/sampler.rs b/src/view/sampler.rs index b431292f..1679738f 100644 --- a/src/view/sampler.rs +++ b/src/view/sampler.rs @@ -6,7 +6,7 @@ pub fn render (state: &Sampler, buf: &mut Buffer, Rect { x, y, height, .. }: Rec { let style = Style::default().gray(); let title = format!(" {} ({})", state.name, state.voices.len()); - title.blit(buf, x+1, y, Some(style.white().bold().not_dim())); + title.blit(buf, x+1, y, Some(style.white().bold().not_dim()))?; let mut width = title.len() + 2; for (i, (note, sample)) in state.samples.iter().enumerate() { let style = if i == state.cursor.0 { @@ -20,15 +20,15 @@ pub fn render (state: &Sampler, buf: &mut Buffer, Rect { x, y, height, .. }: Rec break } if i as usize == state.cursor.0 { - "⯈".blit(buf, x+1, y1, Some(style.bold())); + "⯈".blit(buf, x+1, y1, Some(style.bold()))?; } let label1 = format!("{note:3} {:8}", sample.name); let label2 = format!("{:>6} {:>6}", sample.start, sample.end); - label1.blit(buf, x+2, y1, Some(style.bold())); - label2.blit(buf, x+3+label1.len()as u16, y1, Some(style)); + label1.blit(buf, x+2, y1, Some(style.bold()))?; + label2.blit(buf, x+3+label1.len()as u16, y1, Some(style))?; width = width.max(label1.len() + label2.len() + 4); } - let height = ((1 + state.samples.len()) as u16).min(height); + let height = ((2 + state.samples.len()) as u16).min(height); Ok(Rect { x, y, width: width as u16, height }) } diff --git a/src/view/sequencer.rs b/src/view/sequencer.rs index bc64f35f..b9f0b088 100644 --- a/src/view/sequencer.rs +++ b/src/view/sequencer.rs @@ -1,5 +1,82 @@ use crate::{core::*,model::*}; +pub struct BufferedSequencerView { + pub ppq: usize, + pub length: usize, + pub character: [Vec;64], + pub fg: [Vec;64], + pub bg: [Vec;64], + pub notes: [bool;128], + /// 1st time step to displayRange of time steps to display + pub time_start: usize, + /// PPQ per display unit + pub time_zoom: usize, +} +render!(BufferedSequencerView |self, buf, area| { + let mut area = area; + area.height = area.height.min(64); + for y in 0..area.height { + for x in 0..area.width { + let cell = buf.get_mut(area.x + x, area.y + y); + let time_index = (self.time_start + x as usize) * self.time_zoom; + let note_index = 63 - y as usize; + cell.set_char(self.character[note_index][time_index]); + } + } + Ok(area) +}); +impl BufferedSequencerView { + pub fn new (ppq: usize, length: usize) -> Self { + let dots: Vec = vec!['·';length] + .iter() + .enumerate() + .map(|(i,x)|if i % ppq == 0 { '|' } else { *x }) + .collect(); + Self { + ppq, + length, + character: core::array::from_fn(|_|dots.clone()), + fg: core::array::from_fn(|_|vec![Color::Reset;length]), + bg: core::array::from_fn(|_|vec![Color::Reset;length]), + notes: [false;128], + time_start: 0, + time_zoom: 12, + } + } + pub fn update (&mut self, phrase: Option<&Phrase>) { + if phrase.is_none() { + return + } + let phrase = phrase.unwrap(); + if phrase.length != self.length { + *self = Self::new(self.ppq, phrase.length); + } + self.notes = [false;128]; + for (x, messages) in phrase.notes.iter().enumerate() { + for message in messages.iter() { + match message { + MidiMessage::NoteOn { key, .. } => { + self.notes[key.as_int() as usize] = true; + }, + MidiMessage::NoteOff { key, .. } => { + self.notes[key.as_int() as usize] = false; + }, + _ => {} + } + } + for y in 0..64 { + let note1 = self.notes[y * 2]; + let note2 = self.notes[y * 2 + 1]; + if let Some(block) = half_block(note1, note2) { + self.character[63 - y][x] = block; + } else if x % self.ppq == 0 { + self.character[63 - y][x] = '|'; + } + } + } + } +} + pub struct SequencerView<'a> { focused: bool, /// Displayed phrase @@ -37,8 +114,8 @@ impl<'a> SequencerView<'a> { ppq: app.timebase.ppq() as usize, now: app.timebase.frame_to_pulse(app.playhead as f64) as usize, time_cursor: app.time_cursor, - time_start: app.time_start, - time_zoom: app.time_zoom, + time_start: app.seq_buf.time_start, + time_zoom: app.seq_buf.time_zoom, note_cursor: app.note_cursor, note_start: app.note_start, notes_in: if let Some(track) = track { &track.notes_in } else { &[false;128] },