diff --git a/crates/tek_sequencer/src/lib.rs b/crates/tek_sequencer/src/lib.rs index a203364d..ad8db9c9 100644 --- a/crates/tek_sequencer/src/lib.rs +++ b/crates/tek_sequencer/src/lib.rs @@ -11,8 +11,8 @@ use rand::distributions::uniform::UniformSampler; submod! { arranger arranger_cmd arranger_tui - sequencer sequencer_cmd sequencer_tui - transport transport_cmd transport_tui + sequencer sequencer_cmd sequencer_tui sequencer_snd + transport transport_cmd transport_tui transport_snd } pub const PPQ: usize = 96; diff --git a/crates/tek_sequencer/src/sequencer.rs b/crates/tek_sequencer/src/sequencer.rs index d8099323..6644e929 100644 --- a/crates/tek_sequencer/src/sequencer.rs +++ b/crates/tek_sequencer/src/sequencer.rs @@ -102,7 +102,7 @@ pub struct PhrasePlayer { /// Output from current sequence. pub midi_out: Option>, /// MIDI output buffer - midi_out_buf: Vec>>, + pub midi_out_buf: Vec>>, /// Send all notes off pub reset: bool, // TODO?: after Some(nframes) } @@ -148,6 +148,49 @@ impl PhrasePool { } return None } + pub fn len (&self) -> usize { + self.phrases.len() + } + pub fn index_before (&self, index: usize) -> usize { + index.overflowing_sub(1).0.min(self.len() - 1) + } + pub fn index_after (&self, index: usize) -> usize { + (index + 1) % self.len() + } + pub fn select_prev (&mut self) { + self.phrase = self.index_before(self.phrase) + } + pub fn select_next (&mut self) { + self.phrase = self.index_after(self.phrase) + } + pub fn append_new (&mut self, name: Option<&str>, color: Option) { + let mut phrase = Phrase::default(); + phrase.name = String::from(name.unwrap_or("(no name)")); + phrase.color = color.unwrap_or_else(random_color); + self.phrases.push(Arc::new(RwLock::new(phrase))); + self.phrase = self.phrases.len() - 1; + } + pub fn insert_new (&mut self, name: Option<&str>, color: Option) { + let mut phrase = Phrase::default(); + phrase.name = String::from(name.unwrap_or("(no name)")); + phrase.color = color.unwrap_or_else(random_color); + self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); + self.phrase += 1; + } + pub fn insert_dup (&mut self) { + let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); + phrase.color = random_color_near(phrase.color, 0.2); + self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); + self.phrase += 1; + } + pub fn randomize_color (&mut self) { + let mut phrase = self.phrases[self.phrase].write().unwrap(); + phrase.color = random_color(); + } + pub fn begin_rename (&mut self) { + let phrase = self.phrases[self.phrase].read().unwrap(); + self.mode = Some(PhrasePoolMode::Rename(self.phrase, phrase.name.clone())); + } } impl PhraseEditor { pub fn new () -> Self { @@ -218,33 +261,6 @@ impl Phrase { } return false } - /// Write a chunk of MIDI events to an output port. - pub fn process_out ( - &self, - output: &mut PhraseChunk, - 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, - _ => {} - } - } - } - } } impl Default for Phrase { fn default () -> Self { Self::new("(empty)", false, 0, None, Some(Color::Rgb(0, 0, 0))) } @@ -280,130 +296,4 @@ impl PhrasePlayer { 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(ref phrase) - ) = (playing, started, &self.phrase) { - phrase.read().map(|phrase|{ - if self.midi_out.is_some() { - phrase.process_out( - &mut self.midi_out_buf, - &mut self.notes_out.write().unwrap(), - 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 - let mut notes_in = self.notes_in.write().unwrap(); - 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, .. } => { - notes_in[key.as_int() as usize] = true; - } - MidiMessage::NoteOff { key, .. } => { - notes_in[key.as_int() as usize] = false; - }, - _ => {} - } - }, - _ => {} - } - } - } - } else if input.is_some() && self.midi_out.is_some() && self.monitoring { - let mut notes_in = self.notes_in.write().unwrap(); - for (frame, event, bytes) in parse_midi_input(input.unwrap()) { - match event { - LiveEvent::Midi { message, .. } => { - self.midi_out_buf[frame].push(bytes.to_vec()); - match message { - MidiMessage::NoteOn { key, .. } => { - notes_in[key.as_int() as usize] = true; - } - MidiMessage::NoteOff { key, .. } => { - notes_in[key.as_int() as usize] = false; - }, - _ => {} - } - }, - _ => {} - } - } - } - if let Some(out) = &mut self.midi_out { - let writer = &mut out.writer(scope); - let output = &self.midi_out_buf; - for time in 0..frames { - for event in output[time].iter() { - writer.write(&RawMidi { time: time as u32, bytes: &event }) - .expect(&format!("{event:?}")); - } - } - } - } -} -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut PhraseChunk) { - 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); -} -/// Return boxed iterator of MIDI events -pub fn parse_midi_input (input: MidiIter) -> Box + '_> { - Box::new(input.map(|RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) } diff --git a/crates/tek_sequencer/src/sequencer_cmd.rs b/crates/tek_sequencer/src/sequencer_cmd.rs index 6484b2d1..2f06cee5 100644 --- a/crates/tek_sequencer/src/sequencer_cmd.rs +++ b/crates/tek_sequencer/src/sequencer_cmd.rs @@ -1,5 +1,5 @@ use crate::*; -/// Handle top-level events in standalone arranger. +/// Handle top-level events in standalone sequencer. impl Handle for Sequencer { fn handle (&mut self, from: &TuiInput) -> Perhaps { if !match self.focused() { @@ -44,42 +44,13 @@ impl Handle for PhrasePool { } }, None => match from.event() { - key!(KeyCode::Up) => self.phrase = if self.phrase > 0 { - self.phrase - 1 - } else { - self.phrases.len() - 1 - }, - key!(KeyCode::Down) => { - self.phrase = (self.phrase + 1) % self.phrases.len() - }, - key!(KeyCode::Char('a')) => { // append new - let mut phrase = Phrase::default(); - phrase.name = String::from("(no name)"); - phrase.color = random_color(); - self.phrases.push(Arc::new(RwLock::new(phrase))); - self.phrase = self.phrases.len() - 1; - }, - key!(KeyCode::Char('i')) => { // insert new - let mut phrase = Phrase::default(); - phrase.name = String::from("(no name)"); - phrase.color = random_color(); - self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); - self.phrase += 1; - }, - key!(KeyCode::Char('d')) => { // insert duplicate - let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); - phrase.color = random_color_near(phrase.color, 0.2); - self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); - self.phrase += 1; - }, - key!(KeyCode::Char('c')) => { // change color - let mut phrase = self.phrases[self.phrase].write().unwrap(); - phrase.color = random_color(); - }, - key!(KeyCode::Char('n')) => { // change name - let phrase = self.phrases[self.phrase].read().unwrap(); - self.mode = Some(PhrasePoolMode::Rename(self.phrase, phrase.name.clone())); - }, + key!(KeyCode::Up) => { self.select_prev() }, + key!(KeyCode::Down) => { self.select_next() }, + key!(KeyCode::Char('a')) => { self.append_new(None, None) }, + key!(KeyCode::Char('i')) => { self.insert_new(None, None) }, + key!(KeyCode::Char('d')) => { self.insert_dup() }, + key!(KeyCode::Char('c')) => { self.randomize_color() }, + key!(KeyCode::Char('n')) => { self.begin_rename() }, _ => return Ok(None), } } diff --git a/crates/tek_sequencer/src/sequencer_snd.rs b/crates/tek_sequencer/src/sequencer_snd.rs new file mode 100644 index 00000000..aece30d6 --- /dev/null +++ b/crates/tek_sequencer/src/sequencer_snd.rs @@ -0,0 +1,158 @@ +use crate::*; +impl Phrase { + /// Write a chunk of MIDI events to an output port. + pub fn process_out ( + &self, + output: &mut PhraseChunk, + 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, + _ => {} + } + } + } + } +} +impl PhrasePlayer { + 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(ref phrase) + ) = (playing, started, &self.phrase) { + phrase.read().map(|phrase|{ + if self.midi_out.is_some() { + phrase.process_out( + &mut self.midi_out_buf, + &mut self.notes_out.write().unwrap(), + 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 + let mut notes_in = self.notes_in.write().unwrap(); + 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, .. } => { + notes_in[key.as_int() as usize] = true; + } + MidiMessage::NoteOff { key, .. } => { + notes_in[key.as_int() as usize] = false; + }, + _ => {} + } + }, + _ => {} + } + } + } + } else if input.is_some() && self.midi_out.is_some() && self.monitoring { + let mut notes_in = self.notes_in.write().unwrap(); + for (frame, event, bytes) in parse_midi_input(input.unwrap()) { + match event { + LiveEvent::Midi { message, .. } => { + self.midi_out_buf[frame].push(bytes.to_vec()); + match message { + MidiMessage::NoteOn { key, .. } => { + notes_in[key.as_int() as usize] = true; + } + MidiMessage::NoteOff { key, .. } => { + notes_in[key.as_int() as usize] = false; + }, + _ => {} + } + }, + _ => {} + } + } + } + if let Some(out) = &mut self.midi_out { + let writer = &mut out.writer(scope); + let output = &self.midi_out_buf; + for time in 0..frames { + for event in output[time].iter() { + writer.write(&RawMidi { time: time as u32, bytes: &event }) + .expect(&format!("{event:?}")); + } + } + } + } +} +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut PhraseChunk) { + 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); +} +/// Return boxed iterator of MIDI events +pub fn parse_midi_input (input: MidiIter) -> Box + '_> { + Box::new(input.map(|RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} diff --git a/crates/tek_sequencer/src/transport_snd.rs b/crates/tek_sequencer/src/transport_snd.rs new file mode 100644 index 00000000..e69de29b