mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
flatten workspace into 1 crate
This commit is contained in:
parent
7c4e1e2166
commit
d926422c67
147 changed files with 66 additions and 126 deletions
109
src/midi/midi_clip.rs
Normal file
109
src/midi/midi_clip.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasMidiClip {
|
||||
fn phrase (&self) -> &Arc<RwLock<MidiClip>>;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_phrase {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn phrase (&$self) -> &Arc<RwLock<MidiClip>> { &$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A MIDI sequence.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiClip {
|
||||
pub uuid: uuid::Uuid,
|
||||
/// Name of phrase
|
||||
pub name: String,
|
||||
/// Temporal resolution in pulses per quarter note
|
||||
pub ppq: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub length: usize,
|
||||
/// Notes in phrase
|
||||
pub notes: MidiData,
|
||||
/// Whether to loop the phrase or play it once
|
||||
pub looped: 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: ItemPalette,
|
||||
}
|
||||
|
||||
/// MIDI message structural
|
||||
pub type MidiData = Vec<Vec<MidiMessage>>;
|
||||
|
||||
impl MidiClip {
|
||||
pub fn new (
|
||||
name: impl AsRef<str>,
|
||||
looped: bool,
|
||||
length: usize,
|
||||
notes: Option<MidiData>,
|
||||
color: Option<ItemPalette>,
|
||||
) -> Self {
|
||||
Self {
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
name: name.as_ref().to_string(),
|
||||
ppq: PPQ,
|
||||
length,
|
||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||
looped,
|
||||
loop_start: 0,
|
||||
loop_length: length,
|
||||
percussive: true,
|
||||
color: color.unwrap_or_else(ItemPalette::random)
|
||||
}
|
||||
}
|
||||
pub fn set_length (&mut self, length: usize) {
|
||||
self.length = length;
|
||||
self.notes = vec![Vec::with_capacity(16);length];
|
||||
}
|
||||
pub fn duplicate (&self) -> Self {
|
||||
let mut clone = self.clone();
|
||||
clone.uuid = uuid::Uuid::new_v4();
|
||||
clone
|
||||
}
|
||||
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
|
||||
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 {
|
||||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||
for event in events.iter() {
|
||||
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MidiClip {
|
||||
fn default () -> Self {
|
||||
Self::new(
|
||||
"Stop",
|
||||
false,
|
||||
1,
|
||||
Some(vec![vec![MidiMessage::Controller {
|
||||
controller: 123.into(),
|
||||
value: 0.into()
|
||||
}]]),
|
||||
Some(ItemColor::from(Color::Rgb(32, 32, 32)).into())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MidiClip {
|
||||
fn eq (&self, other: &Self) -> bool {
|
||||
self.uuid == other.uuid
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MidiClip {}
|
||||
225
src/midi/midi_editor.rs
Normal file
225
src/midi/midi_editor.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
use crate::*;
|
||||
use KeyCode::{Char, Up, Down, Left, Right, Enter};
|
||||
use PhraseCommand::*;
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &MidiEditorModel;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_editor {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn editor (&$self) -> &MidiEditorModel { &$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PhraseCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||
AppendNote,
|
||||
PutNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
SetTimeLock(bool),
|
||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||
}
|
||||
|
||||
event_map_input_to_command!(Tui: MidiEditorModel: PhraseCommand: MidiEditorModel::KEYS);
|
||||
impl MidiEditorModel {
|
||||
const KEYS: KeyMapping<31, Self> = [
|
||||
(kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)),
|
||||
(kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))),
|
||||
(kexp!(Ctrl-Alt-Left), &|s: &Self|SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get()))),
|
||||
(kexp!(Ctrl-Alt-Right), &|s: &Self|SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
|
||||
(kexp!(Ctrl-Up), &|s: &Self|SetNoteScroll(s.note_lo().get() + 1)),
|
||||
(kexp!(Ctrl-Down), &|s: &Self|SetNoteScroll(s.note_lo().get().saturating_sub(1))),
|
||||
(kexp!(Ctrl-Left), &|s: &Self|SetTimeScroll(s.time_start().get().saturating_sub(s.note_len()))),
|
||||
(kexp!(Ctrl-Right), &|s: &Self|SetTimeScroll(s.time_start().get() + s.note_len())),
|
||||
(kexp!(Alt-Up), &|s: &Self|SetNoteCursor(s.note_point() + 3)),
|
||||
(kexp!(Alt-Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(3))),
|
||||
(kexp!(Alt-Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get()))),
|
||||
(kexp!(Alt-Right), &|s: &Self|SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
|
||||
(kexp!(Up), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
|
||||
(kexp!(Char('w')), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
|
||||
(kexp!(Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
|
||||
(kexp!(Char('s')), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
|
||||
(kexp!(Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
|
||||
(kexp!(Char('a')), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
|
||||
(kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
|
||||
(kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
|
||||
(kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())),
|
||||
(kexp!(Char('-')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
|
||||
(kexp!(Char('_')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
|
||||
(kexp!(Char('=')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
|
||||
(kexp!(Char('+')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
|
||||
(kexp!(Enter), &|s: &Self|PutNote),
|
||||
(kexp!(Ctrl-Enter), &|s: &Self|AppendNote),
|
||||
(kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet
|
||||
(kexp!(Char('.')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
|
||||
(kexp!(Char('<')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: 3plet
|
||||
(kexp!(Char('>')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
|
||||
//// TODO: key_pat!(Char('/')) => // toggle 3plet
|
||||
//// TODO: key_pat!(Char('?')) => // toggle dotted
|
||||
];
|
||||
fn phrase_length (&self) -> usize {
|
||||
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<MidiEditorModel> for PhraseCommand {
|
||||
fn execute (self, state: &mut MidiEditorModel) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
match self {
|
||||
Show(phrase) => { state.set_phrase(phrase.as_ref()); },
|
||||
PutNote => { state.put_note(false); },
|
||||
AppendNote => { state.put_note(true); },
|
||||
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
||||
SetTimeLock(x) => { state.time_lock().set(x); },
|
||||
SetTimeScroll(x) => { state.time_start().set(x); },
|
||||
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
|
||||
SetNoteLength(x) => { state.set_note_len(x); },
|
||||
SetTimeCursor(x) => { state.set_time_point(x); },
|
||||
SetNoteCursor(note) => { state.set_note_point(note.min(127)); },
|
||||
_ => todo!("{:?}", self)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct MidiEditorModel {
|
||||
/// Renders the phrase
|
||||
pub mode: Box<dyn PhraseViewMode>,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl Default for MidiEditorModel {
|
||||
fn default () -> Self {
|
||||
let mut mode = Box::new(PianoHorizontal::new(None));
|
||||
mode.redraw();
|
||||
Self { mode, size: Measure::new() }
|
||||
}
|
||||
}
|
||||
|
||||
has_size!(<Tui>|self:MidiEditorModel|&self.size);
|
||||
render!(<Tui>|self: MidiEditorModel|{
|
||||
self.autoscroll();
|
||||
self.autozoom();
|
||||
&self.mode
|
||||
});
|
||||
//render!(<Tui>|self: MidiEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
|
||||
|
||||
pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + Debug + Send + Sync {
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize);
|
||||
fn redraw (&mut self);
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
*self.phrase_mut() = phrase.cloned();
|
||||
self.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiView<Tui> for MidiEditorModel {}
|
||||
|
||||
impl TimeRange for MidiEditorModel {
|
||||
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
||||
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
||||
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
|
||||
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
|
||||
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
||||
}
|
||||
|
||||
impl NoteRange for MidiEditorModel {
|
||||
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
||||
}
|
||||
|
||||
impl NotePoint for MidiEditorModel {
|
||||
fn note_len (&self) -> usize { self.mode.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) }
|
||||
fn note_point (&self) -> usize { self.mode.note_point() }
|
||||
fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) }
|
||||
}
|
||||
|
||||
impl TimePoint for MidiEditorModel {
|
||||
fn time_point (&self) -> usize { self.mode.time_point() }
|
||||
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
|
||||
}
|
||||
|
||||
impl PhraseViewMode for MidiEditorModel {
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
|
||||
self.mode.buffer_size(phrase)
|
||||
}
|
||||
fn redraw (&mut self) {
|
||||
self.mode.redraw()
|
||||
}
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
||||
self.mode.phrase()
|
||||
}
|
||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
self.mode.phrase_mut()
|
||||
}
|
||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
self.mode.set_phrase(phrase)
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiEditorModel {
|
||||
/// Put note at current position
|
||||
pub fn put_note (&mut self, advance: bool) {
|
||||
let mut redraw = false;
|
||||
if let Some(phrase) = self.phrase() {
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let note_start = self.time_point();
|
||||
let note_point = self.note_point();
|
||||
let note_len = self.note_len();
|
||||
let note_end = note_start + (note_len.saturating_sub(1));
|
||||
let key: u7 = u7::from(note_point as u8);
|
||||
let vel: u7 = 100.into();
|
||||
let length = phrase.length;
|
||||
let note_end = note_end % length;
|
||||
let note_on = MidiMessage::NoteOn { key, vel };
|
||||
if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) {
|
||||
phrase.notes[note_start].push(note_on);
|
||||
}
|
||||
let note_off = MidiMessage::NoteOff { key, vel };
|
||||
if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) {
|
||||
phrase.notes[note_end].push(note_off);
|
||||
}
|
||||
if advance {
|
||||
self.set_time_point(note_end);
|
||||
}
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
self.mode.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditorModel = {
|
||||
let mut model = Self::from(Some(phrase.clone()));
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditorModel = {
|
||||
let mut model = Self::default();
|
||||
*model.phrase_mut() = phrase;
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
impl std::fmt::Debug for MidiEditorModel {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiEditorModel")
|
||||
.field("mode", &self.mode)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
34
src/midi/midi_launch.rs
Normal file
34
src/midi/midi_launch.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPlayPhrase: HasClock {
|
||||
fn reset (&self) -> bool;
|
||||
fn reset_mut (&mut self) -> &mut bool;
|
||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn pulses_since_start (&self) -> Option<f64> {
|
||||
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
Some(elapsed)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn pulses_since_start_looped (&self) -> Option<f64> {
|
||||
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase
|
||||
let elapsed = (elapsed as usize % length) as f64;
|
||||
Some(elapsed)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
let start = self.clock().next_launch_pulse() as f64;
|
||||
let instant = Moment::from_pulse(self.clock().timebase(), start);
|
||||
*self.next_phrase_mut() = Some((instant, phrase.cloned()));
|
||||
*self.reset_mut() = true;
|
||||
}
|
||||
}
|
||||
36
src/midi/midi_note.rs
Normal file
36
src/midi/midi_note.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Note;
|
||||
|
||||
impl Note {
|
||||
/// (pulses, name), assuming 96 PPQ
|
||||
pub const DURATIONS: [(usize, &str);26] = [
|
||||
(1, "1/384"), (2, "1/192"),
|
||||
(3, "1/128"), (4, "1/96"),
|
||||
(6, "1/64"), (8, "1/48"),
|
||||
(12, "1/32"), (16, "1/24"),
|
||||
(24, "1/16"), (32, "1/12"),
|
||||
(48, "1/8"), (64, "1/6"),
|
||||
(96, "1/4"), (128, "1/3"),
|
||||
(192, "1/2"), (256, "2/3"),
|
||||
(384, "1/1"), (512, "4/3"),
|
||||
(576, "3/2"), (768, "2/1"),
|
||||
(1152, "3/1"), (1536, "4/1"),
|
||||
(2304, "6/1"), (3072, "8/1"),
|
||||
(3456, "9/1"), (6144, "16/1"),
|
||||
];
|
||||
/// Returns the next shorter length
|
||||
pub fn prev (pulses: usize) -> usize {
|
||||
for i in 1..=16 { let length = Note::DURATIONS[16-i].0; if length < pulses { return length } }
|
||||
pulses
|
||||
}
|
||||
/// Returns the next longer length
|
||||
pub fn next (pulses: usize) -> usize {
|
||||
for (length, _) in &Note::DURATIONS { if *length > pulses { return *length } }
|
||||
pulses
|
||||
}
|
||||
pub fn pulses_to_name (pulses: usize) -> &'static str {
|
||||
for (length, name) in &Note::DURATIONS { if *length == pulses { return name } }
|
||||
""
|
||||
}
|
||||
}
|
||||
149
src/midi/midi_play.rs
Normal file
149
src/midi/midi_play.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
fn clear (
|
||||
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
|
||||
) {
|
||||
for frame in &mut out[0..scope.n_frames() as usize] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Output notes from phrase to MIDI output ports.
|
||||
fn play (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) -> bool {
|
||||
if !self.clock().is_rolling() {
|
||||
return false
|
||||
}
|
||||
// If a phrase is playing, write a chunk of MIDI events from it to the output buffer.
|
||||
// If no phrase is playing, prepare for switchover immediately.
|
||||
self.play_phrase().as_ref().map_or(true, |(started, phrase)|{
|
||||
self.play_chunk(scope, note_buf, out, started, phrase)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle switchover from current to next playing phrase.
|
||||
fn switchover (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) {
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
//let samples = scope.n_frames() as usize;
|
||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||
let start = start_at.sample.get() as usize;
|
||||
let sample = self.clock().started.read().unwrap()
|
||||
.as_ref().unwrap().sample.get() as usize;
|
||||
// If it's time to switch to the next phrase:
|
||||
if start <= sample0.saturating_sub(sample) {
|
||||
// Samples elapsed since phrase was supposed to start
|
||||
let _skipped = sample0 - start;
|
||||
// Switch over to enqueued phrase
|
||||
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
||||
// Launch enqueued phrase
|
||||
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
||||
// Unset enqueuement (TODO: where to implement looping?)
|
||||
*self.next_phrase_mut() = None;
|
||||
// Fill in remaining ticks of chunk from next phrase.
|
||||
self.play(scope, note_buf, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_chunk (
|
||||
&self,
|
||||
scope: &ProcessScope,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
started: &Moment,
|
||||
phrase: &Option<Arc<RwLock<MidiClip>>>
|
||||
) -> bool {
|
||||
// First sample to populate. Greater than 0 means that the first
|
||||
// pulse of the phrase falls somewhere in the middle of the chunk.
|
||||
let sample = (scope.last_frame_time() as usize).saturating_sub(
|
||||
started.sample.get() as usize +
|
||||
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
|
||||
);
|
||||
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
||||
// paired with pulse (index into phrase from which to take the MIDI event) for each
|
||||
// sample of the output buffer that corresponds to a MIDI pulse.
|
||||
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
|
||||
// Notes active during current chunk.
|
||||
let notes = &mut self.notes_out().write().unwrap();
|
||||
let length = phrase.as_ref().map_or(0, |p|p.read().unwrap().length);
|
||||
for (sample, pulse) in pulses {
|
||||
// If a next phrase is enqueued, and we're past the end of the current one,
|
||||
// break the loop here (FIXME count pulse correctly)
|
||||
let past_end = if phrase.is_some() { pulse >= length } else { true };
|
||||
if self.next_phrase().is_some() && past_end {
|
||||
return true
|
||||
}
|
||||
// If there's a currently playing phrase, output notes from it to buffer:
|
||||
if let Some(ref phrase) = phrase {
|
||||
Self::play_pulse(phrase, pulse, sample, note_buf, out, notes)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn play_pulse (
|
||||
phrase: &RwLock<MidiClip>,
|
||||
pulse: usize,
|
||||
sample: usize,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
notes: &mut [bool;128]
|
||||
) {
|
||||
// Source phrase from which the MIDI events will be taken.
|
||||
let phrase = phrase.read().unwrap();
|
||||
// Phrase with zero length is not processed
|
||||
if phrase.length > 0 {
|
||||
// Current pulse index in source phrase
|
||||
let pulse = pulse % phrase.length;
|
||||
// Output each MIDI event from phrase at appropriate frames of output buffer:
|
||||
for message in phrase.notes[pulse].iter() {
|
||||
// Clear output buffer for this MIDI event.
|
||||
note_buf.clear();
|
||||
// TODO: support MIDI channels other than CH1.
|
||||
let channel = 0.into();
|
||||
// Serialize MIDI event into message buffer.
|
||||
LiveEvent::Midi { channel, message: *message }
|
||||
.write(note_buf)
|
||||
.unwrap();
|
||||
// Append serialized message to output buffer.
|
||||
out[sample].push(note_buf.clone());
|
||||
// Update the list of currently held notes.
|
||||
update_keys(&mut*notes, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
|
||||
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
Self::write_port(&mut port.writer(scope), samples, out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to an output port.
|
||||
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
|
||||
for (time, events) in out.iter().enumerate().take(samples) {
|
||||
for bytes in events.iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
|
||||
panic!("Failed to write MIDI data: {bytes:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/midi/midi_point.rs
Normal file
50
src/midi/midi_point.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiPointModel {
|
||||
/// Time coordinate of cursor
|
||||
pub time_point: Arc<AtomicUsize>,
|
||||
/// Note coordinate of cursor
|
||||
pub note_point: Arc<AtomicUsize>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub note_len: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Default for MidiPointModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
time_point: Arc::new(0.into()),
|
||||
note_point: Arc::new(36.into()),
|
||||
note_len: Arc::new(24.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NotePoint {
|
||||
fn note_len (&self) -> usize;
|
||||
fn set_note_len (&self, x: usize);
|
||||
fn note_point (&self) -> usize;
|
||||
fn set_note_point (&self, x: usize);
|
||||
fn note_end (&self) -> usize { self.note_point() + self.note_len() }
|
||||
}
|
||||
|
||||
pub trait TimePoint {
|
||||
fn time_point (&self) -> usize;
|
||||
fn set_time_point (&self, x: usize);
|
||||
}
|
||||
|
||||
pub trait MidiPoint: NotePoint + TimePoint {}
|
||||
|
||||
impl<T: NotePoint + TimePoint> MidiPoint for T {}
|
||||
|
||||
impl NotePoint for MidiPointModel {
|
||||
fn note_len (&self) -> usize { self.note_len.load(Relaxed)}
|
||||
fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) }
|
||||
fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) }
|
||||
fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) }
|
||||
}
|
||||
|
||||
impl TimePoint for MidiPointModel {
|
||||
fn time_point (&self) -> usize { self.time_point.load(Relaxed) }
|
||||
fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) }
|
||||
}
|
||||
93
src/midi/midi_pool.rs
Normal file
93
src/midi/midi_pool.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPhrases {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<MidiClip>>>;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_phrases {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn phrases (&$self) -> &Vec<Arc<RwLock<MidiClip>>> { &$cb }
|
||||
fn phrases_mut (&mut $self) -> &mut Vec<Arc<RwLock<MidiClip>>> { &mut$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhrasePoolCommand {
|
||||
Add(usize, MidiClip),
|
||||
Delete(usize),
|
||||
Swap(usize, usize),
|
||||
Import(usize, PathBuf),
|
||||
Export(usize, PathBuf),
|
||||
SetName(usize, String),
|
||||
SetLength(usize, usize),
|
||||
SetColor(usize, ItemColor),
|
||||
}
|
||||
|
||||
impl<T: HasPhrases> Command<T> for PhrasePoolCommand {
|
||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||
use PhrasePoolCommand::*;
|
||||
Ok(match self {
|
||||
Add(mut index, phrase) => {
|
||||
let phrase = Arc::new(RwLock::new(phrase));
|
||||
let phrases = model.phrases_mut();
|
||||
if index >= phrases.len() {
|
||||
index = phrases.len();
|
||||
phrases.push(phrase)
|
||||
} else {
|
||||
phrases.insert(index, phrase);
|
||||
}
|
||||
Some(Self::Delete(index))
|
||||
},
|
||||
Delete(index) => {
|
||||
let phrase = model.phrases_mut().remove(index).read().unwrap().clone();
|
||||
Some(Self::Add(index, phrase))
|
||||
},
|
||||
Swap(index, other) => {
|
||||
model.phrases_mut().swap(index, other);
|
||||
Some(Self::Swap(index, other))
|
||||
},
|
||||
Import(index, path) => {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let smf = Smf::parse(bytes.as_slice())?;
|
||||
let mut t = 0u32;
|
||||
let mut events = vec![];
|
||||
for track in smf.tracks.iter() {
|
||||
for event in track.iter() {
|
||||
t += event.delta.as_int();
|
||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||
events.push((t, channel.as_int(), message));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut phrase = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
phrase.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Self::Add(index, phrase).execute(model)?
|
||||
},
|
||||
Export(_index, _path) => {
|
||||
todo!("export phrase to midi file");
|
||||
},
|
||||
SetName(index, name) => {
|
||||
let mut phrase = model.phrases()[index].write().unwrap();
|
||||
let old_name = phrase.name.clone();
|
||||
phrase.name = name;
|
||||
Some(Self::SetName(index, old_name))
|
||||
},
|
||||
SetLength(index, length) => {
|
||||
let mut phrase = model.phrases()[index].write().unwrap();
|
||||
let old_len = phrase.length;
|
||||
phrase.length = length;
|
||||
Some(Self::SetLength(index, old_len))
|
||||
},
|
||||
SetColor(index, color) => {
|
||||
let mut color = ItemPalette::from(color);
|
||||
std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color);
|
||||
Some(Self::SetColor(index, color.base))
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
64
src/midi/midi_range.rs
Normal file
64
src/midi/midi_range.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiRangeModel {
|
||||
pub time_len: Arc<AtomicUsize>,
|
||||
/// Length of visible time axis
|
||||
pub time_axis: Arc<AtomicUsize>,
|
||||
/// Earliest time displayed
|
||||
pub time_start: Arc<AtomicUsize>,
|
||||
/// Time step
|
||||
pub time_zoom: Arc<AtomicUsize>,
|
||||
/// Auto rezoom to fit in time axis
|
||||
pub time_lock: Arc<AtomicBool>,
|
||||
/// Length of visible note axis
|
||||
pub note_axis: Arc<AtomicUsize>,
|
||||
// Lowest note displayed
|
||||
pub note_lo: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
from!(|data:(usize, bool)|MidiRangeModel = Self {
|
||||
time_len: Arc::new(0.into()),
|
||||
note_axis: Arc::new(0.into()),
|
||||
note_lo: Arc::new(0.into()),
|
||||
time_axis: Arc::new(0.into()),
|
||||
time_start: Arc::new(0.into()),
|
||||
time_zoom: Arc::new(data.0.into()),
|
||||
time_lock: Arc::new(data.1.into()),
|
||||
});
|
||||
|
||||
pub trait TimeRange {
|
||||
fn time_len (&self) -> &AtomicUsize;
|
||||
fn time_zoom (&self) -> &AtomicUsize;
|
||||
fn time_lock (&self) -> &AtomicBool;
|
||||
fn time_start (&self) -> &AtomicUsize;
|
||||
fn time_axis (&self) -> &AtomicUsize;
|
||||
fn time_end (&self) -> usize {
|
||||
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NoteRange {
|
||||
fn note_lo (&self) -> &AtomicUsize;
|
||||
fn note_axis (&self) -> &AtomicUsize;
|
||||
fn note_hi (&self) -> usize {
|
||||
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiRange: TimeRange + NoteRange {}
|
||||
|
||||
impl<T: TimeRange + NoteRange> MidiRange for T {}
|
||||
|
||||
impl TimeRange for MidiRangeModel {
|
||||
fn time_len (&self) -> &AtomicUsize { &self.time_len }
|
||||
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
|
||||
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
|
||||
fn time_start (&self) -> &AtomicUsize { &self.time_start }
|
||||
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
|
||||
}
|
||||
|
||||
impl NoteRange for MidiRangeModel {
|
||||
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
||||
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
||||
}
|
||||
82
src/midi/midi_rec.rs
Normal file
82
src/midi/midi_rec.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
fn recording (&self) -> bool;
|
||||
fn recording_mut (&mut self) -> &mut bool;
|
||||
fn toggle_record (&mut self) {
|
||||
*self.recording_mut() = !self.recording();
|
||||
}
|
||||
fn monitoring (&self) -> bool;
|
||||
fn monitoring_mut (&mut self) -> &mut bool;
|
||||
fn toggle_monitor (&mut self) {
|
||||
*self.monitoring_mut() = !self.monitoring();
|
||||
}
|
||||
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
// For highlighting keys and note repeat
|
||||
let notes_in = self.notes_in().clone();
|
||||
let monitoring = self.monitoring();
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
if monitoring {
|
||||
midi_buf[sample].push(bytes.to_vec());
|
||||
}
|
||||
update_keys(&mut notes_in.write().unwrap(), &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
if self.monitoring() {
|
||||
self.monitor(scope, midi_buf);
|
||||
}
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
if let Some((started, ref clip)) = self.play_phrase().clone() {
|
||||
self.record_clip(scope, started, clip, midi_buf);
|
||||
}
|
||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||
self.record_next();
|
||||
}
|
||||
}
|
||||
fn record_clip (
|
||||
&mut self,
|
||||
scope: &ProcessScope,
|
||||
started: Moment,
|
||||
phrase: &Option<Arc<RwLock<MidiClip>>>,
|
||||
midi_buf: &mut Vec<Vec<Vec<u8>>>
|
||||
) {
|
||||
if let Some(phrase) = phrase {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
let start = started.sample.get() as usize;
|
||||
let recording = self.recording();
|
||||
let timebase = self.clock().timebase().clone();
|
||||
let quant = self.clock().quant.get();
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let length = phrase.length;
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
phrase.record_event({
|
||||
let sample = (sample0 + sample - start) as f64;
|
||||
let pulse = timebase.samples_to_pulse(sample);
|
||||
let quantized = (pulse / quant).round() * quant;
|
||||
quantized as usize % length
|
||||
}, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn record_next (&mut self) {
|
||||
// TODO switch to next clip and record into it
|
||||
}
|
||||
fn overdub (&self) -> bool;
|
||||
fn overdub_mut (&mut self) -> &mut bool;
|
||||
fn toggle_overdub (&mut self) {
|
||||
*self.overdub_mut() = !self.overdub();
|
||||
}
|
||||
}
|
||||
26
src/midi/midi_view.rs
Normal file
26
src/midi/midi_view.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait MidiView<E: Engine>: MidiRange + MidiPoint + HasSize<E> {
|
||||
/// Make sure cursor is within range
|
||||
fn autoscroll (&self) {
|
||||
let note_point = self.note_point().min(127);
|
||||
let note_lo = self.note_lo().get();
|
||||
let note_hi = self.note_hi();
|
||||
if note_point < note_lo {
|
||||
self.note_lo().set(note_point);
|
||||
} else if note_point > note_hi {
|
||||
self.note_lo().set((note_lo + note_point).saturating_sub(note_hi));
|
||||
}
|
||||
}
|
||||
/// Make sure range is within display
|
||||
fn autozoom (&self) {
|
||||
let time_len = self.time_len().get();
|
||||
let time_axis = self.time_axis().get();
|
||||
let time_zoom = self.time_zoom().get();
|
||||
//while time_len.div_ceil(time_zoom) > time_axis {
|
||||
//println!("\r{time_len} {time_zoom} {time_axis}");
|
||||
//time_zoom = Note::next(time_zoom);
|
||||
//}
|
||||
//self.time_zoom().set(time_zoom);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue