mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
410 lines
15 KiB
Rust
410 lines
15 KiB
Rust
use crate::*;
|
|
/// MIDI message structural
|
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
/// MIDI message serialized
|
|
pub type PhraseMessage = Vec<u8>;
|
|
/// Collection of serialized MIDI messages
|
|
pub type PhraseChunk = [Vec<PhraseMessage>];
|
|
/// Root level object for standalone `tek_sequencer`
|
|
pub struct Sequencer<E: Engine> {
|
|
/// Which view is focused
|
|
pub focus_cursor: (usize, usize),
|
|
/// Controls the JACK transport.
|
|
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
|
/// Pool of all phrases available to the sequencer
|
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
|
/// Phrase editor view
|
|
pub editor: PhraseEditor<E>,
|
|
}
|
|
/// Sections in the sequencer app that may be focused
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
pub enum SequencerFocus { Transport, PhrasePool, PhraseEditor }
|
|
/// Contains all phrases in a project
|
|
pub struct PhrasePool<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Scroll offset
|
|
pub scroll: usize,
|
|
/// Highlighted phrase
|
|
pub phrase: usize,
|
|
/// Phrases in the pool
|
|
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
|
/// Whether this widget is focused
|
|
pub focused: bool,
|
|
/// Mode switch
|
|
pub mode: Option<PhrasePoolMode>,
|
|
}
|
|
/// Modes for phrase pool
|
|
pub enum PhrasePoolMode { Rename(usize) }
|
|
/// A MIDI sequence.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Phrase {
|
|
pub uuid: uuid::Uuid,
|
|
/// Name of phrase
|
|
pub name: Arc<RwLock<String>>,
|
|
/// Temporal resolution in pulses per quarter note
|
|
pub ppq: usize,
|
|
/// Length of phrase in pulses
|
|
pub length: usize,
|
|
/// Notes in phrase
|
|
pub notes: PhraseData,
|
|
/// Whether to loop the phrase or play it once
|
|
pub loop_on: 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 phrase
|
|
pub color: Color,
|
|
}
|
|
/// Contains state for viewing and editing a phrase
|
|
pub struct PhraseEditor<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Phrase being played
|
|
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
|
/// The full piano keys are rendered to this buffer
|
|
pub keys: Buffer,
|
|
/// The full piano roll is rendered to this buffer
|
|
pub buffer: BigBuffer,
|
|
/// Cursor/scroll/zoom in pitch axis
|
|
pub note_axis: FixedAxis<usize>,
|
|
/// Cursor/scroll/zoom in time axis
|
|
pub time_axis: ScaledAxis<usize>,
|
|
/// Whether this widget is focused
|
|
pub focused: bool,
|
|
/// Whether note enter mode is enabled
|
|
pub entered: bool,
|
|
/// Display mode
|
|
pub mode: bool,
|
|
/// Notes currently held at input
|
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
/// Notes currently held at output
|
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
}
|
|
/// Phrase player.
|
|
pub struct PhrasePlayer<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Phrase being played
|
|
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
|
/// Notes currently held at input
|
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
/// Notes currently held at output
|
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
/// Current point in playing phrase
|
|
pub now: usize,
|
|
/// Play input through output.
|
|
pub monitoring: bool,
|
|
/// Write input to sequence.
|
|
pub recording: bool,
|
|
/// Overdub input to sequence.
|
|
pub overdub: bool,
|
|
/// Output from current sequence.
|
|
pub midi_out: Option<Port<MidiOut>>,
|
|
/// MIDI output buffer
|
|
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
|
/// Send all notes off
|
|
pub reset: bool, // TODO?: after Some(nframes)
|
|
}
|
|
/// Focus layout of sequencer app
|
|
impl<E: Engine> FocusGrid<SequencerFocus> for Sequencer<E> {
|
|
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
|
|
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
|
|
fn layout (&self) -> &[&[SequencerFocus]] { &[
|
|
&[SequencerFocus::Transport],
|
|
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
|
|
] }
|
|
fn update_focus (&mut self) {
|
|
let focused = *self.focused();
|
|
if let Some(transport) = self.transport.as_ref() {
|
|
transport.write().unwrap().focused =
|
|
focused == SequencerFocus::Transport
|
|
}
|
|
self.phrases.write().unwrap().focused =
|
|
focused == SequencerFocus::PhrasePool;
|
|
self.editor.focused =
|
|
focused == SequencerFocus::PhraseEditor;
|
|
}
|
|
}
|
|
impl<E: Engine> PhrasePool<E> {
|
|
pub fn new () -> Self {
|
|
Self {
|
|
_engine: Default::default(),
|
|
scroll: 0,
|
|
phrase: 0,
|
|
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
|
|
focused: false,
|
|
mode: None,
|
|
}
|
|
}
|
|
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> {
|
|
&self.phrases[self.phrase]
|
|
}
|
|
}
|
|
impl<E: Engine> PhraseEditor<E> {
|
|
pub fn new () -> Self {
|
|
Self {
|
|
_engine: Default::default(),
|
|
phrase: None,
|
|
notes_in: Arc::new(RwLock::new([false;128])),
|
|
notes_out: Arc::new(RwLock::new([false;128])),
|
|
keys: keys_vert(),
|
|
buffer: Default::default(),
|
|
note_axis: FixedAxis { start: 12, point: Some(36) },
|
|
time_axis: ScaledAxis { start: 0, scale: 24, point: Some(0) },
|
|
focused: false,
|
|
entered: false,
|
|
mode: false,
|
|
}
|
|
}
|
|
}
|
|
pub fn random_color () -> Color {
|
|
let mut rng = thread_rng();
|
|
let color: Okhsl<f32> = Okhsl::new(
|
|
rng.gen::<f32>() * 360f32 - 180f32,
|
|
rng.gen::<f32>() * 0.5 + 0.25,
|
|
rng.gen::<f32>() * 0.5 + 0.25,
|
|
);
|
|
let color: Srgb<f32> = Srgb::from_color_unclamped(color);
|
|
Color::Rgb(
|
|
(color.red * 255.0) as u8,
|
|
(color.green * 255.0) as u8,
|
|
(color.blue * 255.0) as u8,
|
|
)
|
|
}
|
|
impl Phrase {
|
|
pub fn new (
|
|
name: &str,
|
|
loop_on: bool,
|
|
length: usize,
|
|
notes: Option<PhraseData>,
|
|
color: Option<Color>,
|
|
) -> Self {
|
|
Self {
|
|
uuid: uuid::Uuid::new_v4(),
|
|
name: Arc::new(RwLock::new(name.into())),
|
|
ppq: PPQ,
|
|
length,
|
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
loop_on,
|
|
loop_start: 0,
|
|
loop_length: length,
|
|
percussive: true,
|
|
color: color.unwrap_or_else(random_color)
|
|
}
|
|
}
|
|
pub fn toggle_loop (&mut self) {
|
|
self.loop_on = !self.loop_on;
|
|
}
|
|
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 PhraseChunk,
|
|
notes_on: &mut [bool;128],
|
|
timebase: &Arc<Timebase>,
|
|
(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("", false, 0, None, Some(Color::Rgb(0, 0, 0))) }
|
|
}
|
|
impl std::cmp::PartialEq for Phrase {
|
|
fn eq (&self, other: &Self) -> bool {
|
|
self.uuid == other.uuid
|
|
}
|
|
}
|
|
impl Eq for Phrase {}
|
|
impl<E: Engine> PhrasePlayer<E> {
|
|
pub fn new (name: &str) -> Self {
|
|
Self {
|
|
_engine: Default::default(),
|
|
phrase: None,
|
|
notes_in: Arc::new(RwLock::new([false;128])),
|
|
notes_out: Arc::new(RwLock::new([false;128])),
|
|
monitoring: false,
|
|
recording: false,
|
|
overdub: true,
|
|
midi_out: None,
|
|
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
|
reset: true,
|
|
now: 0,
|
|
}
|
|
}
|
|
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<MidiIter>,
|
|
timebase: &Arc<Timebase>,
|
|
playing: Option<TransportState>,
|
|
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<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
|
Box::new(input.map(|RawMidi { time, bytes }|(
|
|
time as usize,
|
|
LiveEvent::parse(bytes).unwrap(),
|
|
bytes
|
|
)))
|
|
}
|