rename phrase -> clip mostly everywhere

This commit is contained in:
🪞👃🪞 2025-01-10 02:12:31 +01:00
parent 709391ff0a
commit 08f7a62692
24 changed files with 426 additions and 423 deletions

View file

@ -58,42 +58,3 @@ pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
_ => {}
}
}
/// A phrase, rendered as a horizontal piano roll.
pub struct PianoHorizontal {
phrase: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole phrase is rerendered on change
buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
size: Measure<TuiOut>,
/// The display window
range: MidiRangeModel,
/// The note cursor
point: MidiPointModel,
/// The highlight color palette
color: ItemPalette,
/// Width of the keyboard
keys_width: u16,
}
impl PianoHorizontal {
pub fn new (phrase: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((24, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let mut piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
phrase: phrase.cloned(),
color: phrase.as_ref()
.map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))),
};
piano.redraw();
piano
}
}

View file

@ -1,13 +1,13 @@
use crate::*;
pub trait HasMidiClip {
fn phrase (&self) -> &Arc<RwLock<MidiClip>>;
fn clip (&self) -> &Arc<RwLock<MidiClip>>;
}
#[macro_export] macro_rules! has_phrase {
#[macro_export] macro_rules! has_clip {
(|$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 }
fn clip (&$self) -> &Arc<RwLock<MidiClip>> { &$cb }
}
}
}
@ -16,15 +16,15 @@ pub trait HasMidiClip {
#[derive(Debug, Clone)]
pub struct MidiClip {
pub uuid: uuid::Uuid,
/// Name of phrase
/// Name of clip
pub name: Arc<str>,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of phrase in pulses
/// Length of clip in pulses
pub length: usize,
/// Notes in phrase
/// Notes in clip
pub notes: MidiData,
/// Whether to loop the phrase or play it once
/// Whether to loop the clip or play it once
pub looped: bool,
/// Start of loop
pub loop_start: usize,
@ -32,7 +32,7 @@ pub struct MidiClip {
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of phrase
/// Identifying color of clip
pub color: ItemPalette,
}
@ -71,7 +71,7 @@ impl MidiClip {
}
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") }
if pulse >= self.length { panic!("extend clip first") }
self.notes[pulse].push(message);
}
/// Check if a range `start..end` contains MIDI Note On `k`

View file

@ -12,19 +12,19 @@ pub trait HasEditor {
}
}
}
/// Contains state for viewing and editing a phrase
/// Contains state for viewing and editing a clip
pub struct MidiEditor {
pub mode: PianoHorizontal,
pub size: Measure<TuiOut>
}
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditor = {
let model = Self::from(Some(phrase.clone()));
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
let model = Self::from(Some(clip.clone()));
model.redraw();
model
});
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
let mut model = Self::default();
*model.phrase_mut() = phrase;
*model.clip_mut() = clip;
model.redraw();
model
});
@ -63,33 +63,33 @@ impl TimePoint for MidiEditor {
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
}
impl MidiViewer for MidiEditor {
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) { self.mode.buffer_size(phrase) }
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
fn redraw (&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, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_phrase(p) }
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
}
impl MidiEditor {
/// 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();
if let Some(clip) = self.clip() {
let mut clip = clip.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 length = clip.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);
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
clip.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 !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
clip.notes[note_end].push(note_off);
}
if advance {
self.set_time_point(note_end);
@ -102,8 +102,8 @@ impl MidiEditor {
}
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
let (color, name, length, looped) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.name.clone(), clip.length, clip.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false)
};
@ -114,8 +114,8 @@ impl MidiEditor {
}
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
let (color, length) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.length)
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.length)
} else {
(ItemPalette::from(TuiTheme::g(64)), 0)
};
@ -140,7 +140,7 @@ impl std::fmt::Debug for MidiEditor {
}
#[derive(Clone, Debug)]
pub enum MidiEditCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
// TODO: 1-9 seek markers that by default start every 8th of the clip
AppendNote,
PutNote,
SetNoteCursor(usize),
@ -160,11 +160,11 @@ keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
key(Char('s')) => SetNoteCursor(s.note_point().saturating_sub(1)),
key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())),
key(Char('a')) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())),
key(Right) => SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length()),
key(Right) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()),
ctrl(alt(key(Up))) => SetNoteScroll(s.note_point() + 3),
ctrl(alt(key(Down))) => SetNoteScroll(s.note_point().saturating_sub(3)),
ctrl(alt(key(Left))) => SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get())),
ctrl(alt(key(Right))) => SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length()),
ctrl(alt(key(Right))) => SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.clip_length()),
ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1),
ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)),
ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())),
@ -172,8 +172,8 @@ keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
alt(key(Up)) => SetNoteCursor(s.note_point() + 3),
alt(key(Down)) => SetNoteCursor(s.note_point().saturating_sub(3)),
alt(key(Left)) => SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get())),
alt(key(Right)) => SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length()),
key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length()),
alt(key(Right)) => SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.clip_length()),
key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()),
key(Char('z')) => SetTimeLock(!s.time_lock().get()),
key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
@ -189,15 +189,15 @@ keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
//// TODO: kpat!(Char('?')) => // toggle dotted
});
impl MidiEditor {
fn phrase_length (&self) -> usize {
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
fn clip_length (&self) -> usize {
self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
}
}
impl Command<MidiEditor> for MidiEditCommand {
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
use MidiEditCommand::*;
match self {
Show(phrase) => { state.set_phrase(phrase.as_ref()); },
Show(clip) => { state.set_clip(clip.as_ref()); },
PutNote => { state.put_note(false); },
AppendNote => { state.put_note(true); },
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },

View file

@ -44,10 +44,10 @@ pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
if !self.clock().is_rolling() {
return
}
if let Some((started, ref clip)) = self.play_phrase().clone() {
if let Some((started, ref clip)) = self.play_clip().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((start_at, phrase)) = &self.next_phrase() {
if let Some((start_at, clip)) = &self.next_clip() {
self.record_next();
}
}
@ -55,21 +55,21 @@ pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
&mut self,
scope: &ProcessScope,
started: Moment,
phrase: &Option<Arc<RwLock<MidiClip>>>,
clip: &Option<Arc<RwLock<MidiClip>>>,
midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(phrase) = phrase {
if let Some(clip) = clip {
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;
let mut clip = clip.write().unwrap();
let length = clip.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, bytes) in parse_midi_input(input.port.iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
phrase.record_event({
clip.record_event({
let sample = (sample0 + sample - start) as f64;
let pulse = timebase.samples_to_pulse(sample);
let quantized = (pulse / quant).round() * quant;

View file

@ -3,12 +3,12 @@ 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 play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_clip_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() {
if let Some((started, Some(_))) = self.play_clip().as_ref() {
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
Some(elapsed)
} else {
@ -16,9 +16,9 @@ pub trait HasPlayPhrase: HasClock {
}
}
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
if let Some((started, Some(clip))) = self.play_clip().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 length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
let times = (elapsed as usize / length) as f64;
let elapsed = (elapsed as usize % length) as f64;
Some((times, elapsed))
@ -26,10 +26,10 @@ pub trait HasPlayPhrase: HasClock {
None
}
}
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
fn enqueue_next (&mut self, clip: 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.next_clip_mut() = Some((instant, clip.cloned()));
*self.reset_mut() = true;
}
}

View file

@ -28,21 +28,21 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
}
}
/// Output notes from phrase to MIDI output ports.
/// Output notes from clip 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)
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
// If no clip is playing, prepare for switchover immediately.
self.play_clip().as_ref().map_or(true, |(started, clip)|{
self.play_chunk(scope, note_buf, out, started, clip)
})
}
/// Handle switchover from current to next playing phrase.
/// Handle switchover from current to next playing clip.
fn switchover (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) {
@ -51,21 +51,21 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
}
let sample0 = scope.last_frame_time() as usize;
//let samples = scope.n_frames() as usize;
if let Some((start_at, phrase)) = &self.next_phrase() {
if let Some((start_at, clip)) = &self.next_clip() {
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 it's time to switch to the next clip:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since phrase was supposed to start
// Samples elapsed since clip was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued phrase
// Switch over to enqueued clip
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued phrase
*self.play_phrase_mut() = Some((started, phrase.clone()));
// Launch enqueued clip
*self.play_clip_mut() = Some((started, clip.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_phrase_mut() = None;
// Fill in remaining ticks of chunk from next phrase.
*self.next_clip_mut() = None;
// Fill in remaining ticks of chunk from next clip.
self.play(scope, note_buf, out);
}
}
@ -77,52 +77,52 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
phrase: &Option<Arc<RwLock<MidiClip>>>
clip: &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.
// pulse of the clip 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
// paired with pulse (index into clip 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);
let length = clip.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,
// If a next clip 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 {
let past_end = if clip.is_some() { pulse >= length } else { true };
if self.next_clip().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)
// If there's a currently playing clip, output notes from it to buffer:
if let Some(ref clip) = clip {
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
}
}
false
}
fn play_pulse (
phrase: &RwLock<MidiClip>,
clip: &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();
// Source clip from which the MIDI events will be taken.
let clip = clip.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() {
if clip.length > 0 {
// Current pulse index in source clip
let pulse = pulse % clip.length;
// Output each MIDI event from clip at appropriate frames of output buffer:
for message in clip.notes[pulse].iter() {
// Clear output buffer for this MIDI event.
note_buf.clear();
// TODO: support MIDI channels other than CH1.

View file

@ -18,14 +18,14 @@ pub trait HasPlayer {
}
}
/// Contains state for playing a phrase
/// Contains state for playing a clip
pub struct MidiPlayer {
/// State of clock and playhead
pub clock: Clock,
/// Start time and phrase being played
pub play_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next phrase
pub next_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and clip being played
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next clip
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
@ -56,8 +56,8 @@ impl MidiPlayer {
let name = name.as_ref();
let clock = Clock::from(jack);
Ok(Self {
play_phrase: Some((Moment::zero(&clock.timebase), clip.cloned())),
next_phrase: None,
play_clip: Some((Moment::zero(&clock.timebase), clip.cloned())),
next_clip: None,
recording: false,
monitoring: false,
overdub: false,
@ -73,18 +73,18 @@ impl MidiPlayer {
})
}
pub fn play_status (&self) -> impl Content<TuiOut> {
ClipSelected::play_phrase(self)
ClipSelected::play_clip(self)
}
pub fn next_status (&self) -> impl Content<TuiOut> {
ClipSelected::next_phrase(self)
ClipSelected::next_clip(self)
}
}
impl std::fmt::Debug for MidiPlayer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiPlayer")
.field("clock", &self.clock)
.field("play_phrase", &self.play_phrase)
.field("next_phrase", &self.next_phrase)
.field("play_clip", &self.play_clip)
.field("next_clip", &self.next_clip)
.finish()
}
}
@ -97,15 +97,15 @@ from!(|clock: &Clock| MidiPlayer = Self {
recording: false,
monitoring: false,
overdub: false,
play_phrase: None,
next_phrase: None,
play_clip: None,
next_clip: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
});
from!(|state: (&Clock, &Arc<RwLock<MidiClip>>)|MidiPlayer = {
let (clock, phrase) = state;
let (clock, clip) = state;
let mut model = Self::from(clock);
model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone())));
model.play_clip = Some((Moment::zero(&clock.timebase), Some(clip.clone())));
model
});
has_clock!(|self: MidiPlayer|&self.clock);
@ -129,7 +129,7 @@ pub struct PlayerAudio<'a, T: MidiPlayerApi>(
pub &'a mut Vec<Vec<Vec<u8>>>,
);
/// JACK process callback for a sequencer's phrase player/recorder.
/// JACK process callback for a sequencer's clip player/recorder.
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
@ -137,7 +137,7 @@ impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
let midi_buf = &mut self.2;
// Clear output buffer(s)
model.clear(scope, midi_buf, false);
// Write chunk of phrase to output, handle switchover
// Write chunk of clip to output, handle switchover
if model.play(scope, note_buf, midi_buf) {
model.switchover(scope, note_buf, midi_buf);
}
@ -193,17 +193,17 @@ impl HasPlayPhrase for MidiPlayer {
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.play_phrase
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.play_clip
}
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.play_phrase
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.play_clip
}
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.next_phrase
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.next_clip
}
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.next_phrase
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.next_clip
}
}
@ -211,10 +211,10 @@ impl HasPlayPhrase for MidiPlayer {
//pub struct MIDIPlayer {
///// Global timebase
//pub clock: Arc<Clock>,
///// Start time and phrase being played
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
///// Start time and next phrase
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
///// Start time and clip being played
//pub play_clip: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
///// Start time and next clip
//pub next_clip: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
///// Play input through output.
//pub monitoring: bool,
///// Write input to sequence.
@ -247,8 +247,8 @@ impl HasPlayPhrase for MidiPlayer {
//let jack = jack.read().unwrap();
//Ok(Self {
//clock: clock.clone(),
//phrase: None,
//next_phrase: None,
//clip: None,
//next_clip: None,
//notes_in: Arc::new(RwLock::new([false;128])),
//notes_out: Arc::new(RwLock::new([false;128])),
//monitoring: false,

View file

@ -1,15 +1,15 @@
use crate::*;
pub trait HasPhrases {
fn phrases (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<MidiClip>>>;
fn clips (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
fn clips_mut (&mut self) -> &mut Vec<Arc<RwLock<MidiClip>>>;
}
#[macro_export] macro_rules! has_phrases {
#[macro_export] macro_rules! has_clips {
(|$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 }
fn clips (&$self) -> &Vec<Arc<RwLock<MidiClip>>> { &$cb }
fn clips_mut (&mut $self) -> &mut Vec<Arc<RwLock<MidiClip>>> { &mut$cb }
}
}
}
@ -30,23 +30,23 @@ impl<T: HasPhrases> Command<T> for MidiPoolCommand {
fn execute (self, model: &mut T) -> Perhaps<Self> {
use MidiPoolCommand::*;
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)
Add(mut index, clip) => {
let clip = Arc::new(RwLock::new(clip));
let clips = model.clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
phrases.insert(index, phrase);
clips.insert(index, clip);
}
Some(Self::Delete(index))
},
Delete(index) => {
let phrase = model.phrases_mut().remove(index).read().unwrap().clone();
Some(Self::Add(index, phrase))
let clip = model.clips_mut().remove(index).read().unwrap().clone();
Some(Self::Add(index, clip))
},
Swap(index, other) => {
model.phrases_mut().swap(index, other);
model.clips_mut().swap(index, other);
Some(Self::Swap(index, other))
},
Import(index, path) => {
@ -62,30 +62,30 @@ impl<T: HasPhrases> Command<T> for MidiPoolCommand {
}
}
}
let mut phrase = MidiClip::new("imported", true, t as usize + 1, None, None);
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
phrase.notes[event.0 as usize].push(event.2);
clip.notes[event.0 as usize].push(event.2);
}
Self::Add(index, phrase).execute(model)?
Self::Add(index, clip).execute(model)?
},
Export(_index, _path) => {
todo!("export phrase to midi file");
todo!("export clip to midi file");
},
SetName(index, name) => {
let mut phrase = model.phrases()[index].write().unwrap();
let old_name = phrase.name.clone();
phrase.name = name;
let mut clip = model.clips()[index].write().unwrap();
let old_name = clip.name.clone();
clip.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;
let mut clip = model.clips()[index].write().unwrap();
let old_len = clip.length;
clip.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);
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
Some(Self::SetColor(index, color.base))
},
})

View file

@ -12,10 +12,10 @@ render!(TuiOut: (self: ClipSelected) =>
impl ClipSelected {
/// Shows currently playing phrase with beats elapsed
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
let MidiClip { ref name, color, .. } = *phrase.read().unwrap();
/// Shows currently playing clip with beats elapsed
pub fn play_clip <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
let (name, color) = if let Some((_, Some(clip))) = state.play_clip() {
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
(name.clone().into(), color)
} else {
("".to_string().into(), TuiTheme::g(64).into())
@ -32,15 +32,15 @@ impl ClipSelected {
}
}
/// Shows next phrase with beats remaining until switchover
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
/// Shows next clip with beats remaining until switchover
pub fn next_clip <T: HasPlayPhrase> (state: &T) -> Self {
let mut time: Arc<str> = String::from("--.-.--").into();
let mut name: Arc<str> = String::from("").into();
let mut color = ItemPalette::from(TuiTheme::g(64));
if let Some((t, Some(phrase))) = state.next_phrase() {
let phrase = phrase.read().unwrap();
name = phrase.name.clone();
color = phrase.color.clone();
if let Some((t, Some(clip))) = state.next_clip() {
let clip = clip.read().unwrap();
name = clip.name.clone();
color = clip.color.clone();
time = {
let target = t.pulse.get();
let current = state.clock().playhead.pulse.get();
@ -51,12 +51,12 @@ impl ClipSelected {
String::new()
}
}.into()
} else if let Some((t, Some(phrase))) = state.play_phrase() {
let phrase = phrase.read().unwrap();
if phrase.looped {
name = phrase.name.clone();
color = phrase.color.clone();
let target = t.pulse.get() + phrase.length as f64;
} else if let Some((t, Some(clip))) = state.play_clip() {
let clip = clip.read().unwrap();
if clip.looped {
name = clip.name.clone();
color = clip.color.clone();
let target = t.pulse.get() + clip.length as f64;
let current = state.clock().playhead.pulse.get();
if target > current {
time = format!("-{:>}", state.clock().timebase.format_beats_0(

View file

@ -1,12 +1,12 @@
use crate::*;
pub trait MidiViewer: HasSize<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize);
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
fn redraw (&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();
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.redraw();
}
/// Make sure cursor is within note range

View file

@ -1,5 +1,42 @@
use crate::*;
use super::*;
/// A clip, rendered as a horizontal piano roll.
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// The display window
pub range: MidiRangeModel,
/// The note cursor
pub point: MidiPointModel,
/// The highlight color palette
pub color: ItemPalette,
/// Width of the keyboard
pub keys_width: u16,
}
impl PianoHorizontal {
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((24, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let mut piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
clip: clip.cloned(),
color: clip.as_ref()
.map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))),
};
piano.redraw();
piano
}
}
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
@ -10,17 +47,22 @@ render!(TuiOut: (self: PianoHorizontal) => Bsp::s( // the freeze is in the piano
)),
Fill::xy(Bsp::e(
Fixed::x(self.keys_width, PianoHorizontalKeys(self)),
Fill::xy(self.size.of(lay!(self.notes(), self.cursor()))),
Fill::xy(self.size.of("")),
//"",
//Fill::xy(self.size.of(lay!(
////self.notes(),
////self.cursor()
//))),
)),
));
impl PianoHorizontal {
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize, note_len: usize) {
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
for (y, note) in (0..=127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
cell.set_bg(phrase.color.darkest.rgb);
cell.set_fg(phrase.color.darker.rgb);
cell.set_bg(clip.color.darkest.rgb);
cell.set_fg(clip.color.darker.rgb);
cell.set_char(if time % 384 == 0 {
'│'
} else if time % 96 == 0 {
@ -38,10 +80,10 @@ impl PianoHorizontal {
}
}
/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize) {
let style = Style::default().fg(phrase.color.base.rgb);//.bg(Color::Rgb(0, 0, 0));
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
let style = Style::default().fg(clip.color.base.rgb);//.bg(Color::Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() {
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
for (y, note) in (0..=127).rev().enumerate() {
if let Some(cell) = buf.get_mut(x, note) {
@ -53,8 +95,8 @@ impl PianoHorizontal {
}
let time_end = time_start + zoom;
for time in time_start..time_end.min(phrase.length) {
for event in phrase.notes[time].iter() {
for time in time_start..time_end.min(clip.length) {
for event in clip.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
@ -80,31 +122,32 @@ impl PianoHorizontal {
let note_hi = self.note_hi();
let note_point = self.note_point();
let buffer = self.buffer.clone();
RenderThunk::new(move|render: &mut TuiOut|{
let source = buffer.read().unwrap();
let [x0, y0, w, h] = render.area().xywh();
if h as usize != note_axis {
panic!("area height mismatch: {h} <> {note_axis}");
}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
// TODO: enable loop rollover:
//let source_x = (time_start + area_x) % source.width.max(1);
//let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
if let Some(cell) = render.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
*cell = source_cell.clone();
}
}
}
}
}
})
return ""
//RenderThunk::new(move|render: &mut TuiOut|{
//let source = buffer.read().unwrap();
//let [x0, y0, w, h] = render.area().xywh();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
//for (area_x, screen_x) in (x0..x0+w).enumerate() {
//for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
//let source_x = time_start + area_x;
//let source_y = note_hi - area_y;
//// TODO: enable loop rollover:
////let source_x = (time_start + area_x) % source.width.max(1);
////let source_y = (note_hi - area_y) % source.height.max(1);
//let is_in_x = source_x < source.width;
//let is_in_y = source_y < source.height;
//if is_in_x && is_in_y {
//if let Some(source_cell) = source.get(source_x, source_y) {
//if let Some(cell) = render.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
//*cell = source_cell.clone();
//}
//}
//}
//}
//}
//})
}
fn cursor (&self) -> impl Content<TuiOut> {
let style = Some(Style::default().fg(self.color.lightest.rgb));
@ -115,27 +158,28 @@ impl PianoHorizontal {
let time_point = self.time_point();
let time_start = self.time_start().get();
let time_zoom = self.time_zoom().get();
RenderThunk::new(move|render: &mut TuiOut|{
let [x0, y0, w, _] = render.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_point {
for x in 0..w {
let screen_x = x0 + x;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_point && time_point < time_2 {
render.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
render.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})
""
//RenderThunk::new(move|render: &mut TuiOut|{
////let [x0, y0, w, _] = render.area().xywh();
////for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
////if note == note_point {
////for x in 0..w {
////let screen_x = x0 + x;
////let time_1 = time_start + x as usize * time_zoom;
////let time_2 = time_1 + time_zoom;
////if time_1 <= time_point && time_point < time_2 {
////render.blit(&"█", screen_x, screen_y, style);
////let tail = note_len as u16 / time_zoom as u16;
////for x_tail in (screen_x + 1)..(screen_x + tail) {
////render.blit(&"▂", x_tail, screen_y, style);
////}
////break
////}
////}
////break
////}
////}
//})
}
}
@ -163,35 +207,35 @@ impl TimePoint for PianoHorizontal {
fn set_time_point (&self, x: usize) { self.point.set_time_point(x) }
}
impl MidiViewer for PianoHorizontal {
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.phrase
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.clip
}
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.phrase
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.clip
}
/// Determine the required space to render the phrase.
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
(phrase.length / self.range.time_zoom().get(), 128)
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
(clip.length / self.range.time_zoom().get(), 128)
}
fn redraw (&self) {
let buffer = if let Some(phrase) = self.phrase.as_ref() {
let phrase = phrase.read().unwrap();
let buf_size = self.buffer_size(&phrase);
let buffer = if let Some(clip) = self.clip.as_ref() {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.note_len();
let time_zoom = self.time_zoom().get();
self.time_len().set(phrase.length);
PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &phrase, time_zoom);
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {
Default::default()
};
*self.buffer.write().unwrap() = buffer
}
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
*self.phrase_mut() = phrase.cloned();
self.color = phrase.map(|p|p.read().unwrap().color)
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.color = clip.map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
self.redraw();
}
@ -208,12 +252,12 @@ impl std::fmt::Debug for PianoHorizontal {
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//if let Some((ref started_at, Some(ref playing))) = self.player.play_clip {
//let clip = clip.read().unwrap();
//if *playing.read().unwrap() == *clip {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//let now = (pulse - start) % clip.length as f64;
//self.now().set(now);
//}
//}

View file

@ -5,7 +5,7 @@ pub struct PianoHorizontalTimeline<'a>(pub(crate) &'a PianoHorizontal);
render!(TuiOut: |self: PianoHorizontalTimeline<'a>, render|{
let [x, y, w, h] = render.area();
let style = Some(Style::default().dim());
let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let length = self.0.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.0.time_zoom().get();
if t < length {