mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +01:00
rename phrase -> clip mostly everywhere
This commit is contained in:
parent
709391ff0a
commit
08f7a62692
24 changed files with 426 additions and 423 deletions
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait HasMidiClip {
|
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) => {
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MidiClip {
|
pub struct MidiClip {
|
||||||
pub uuid: uuid::Uuid,
|
pub uuid: uuid::Uuid,
|
||||||
/// Name of phrase
|
/// Name of clip
|
||||||
pub name: Arc<str>,
|
pub name: Arc<str>,
|
||||||
/// Temporal resolution in pulses per quarter note
|
/// Temporal resolution in pulses per quarter note
|
||||||
pub ppq: usize,
|
pub ppq: usize,
|
||||||
/// Length of phrase in pulses
|
/// Length of clip in pulses
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
/// Notes in phrase
|
/// Notes in clip
|
||||||
pub notes: MidiData,
|
pub notes: MidiData,
|
||||||
/// Whether to loop the phrase or play it once
|
/// Whether to loop the clip or play it once
|
||||||
pub looped: bool,
|
pub looped: bool,
|
||||||
/// Start of loop
|
/// Start of loop
|
||||||
pub loop_start: usize,
|
pub loop_start: usize,
|
||||||
|
|
@ -32,7 +32,7 @@ pub struct MidiClip {
|
||||||
pub loop_length: usize,
|
pub loop_length: usize,
|
||||||
/// All notes are displayed with minimum length
|
/// All notes are displayed with minimum length
|
||||||
pub percussive: bool,
|
pub percussive: bool,
|
||||||
/// Identifying color of phrase
|
/// Identifying color of clip
|
||||||
pub color: ItemPalette,
|
pub color: ItemPalette,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ impl MidiClip {
|
||||||
}
|
}
|
||||||
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
|
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
|
||||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
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);
|
self.notes[pulse].push(message);
|
||||||
}
|
}
|
||||||
/// Check if a range `start..end` contains MIDI Note On `k`
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||||
|
|
|
||||||
|
|
@ -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 struct MidiEditor {
|
||||||
pub mode: PianoHorizontal,
|
pub mode: PianoHorizontal,
|
||||||
pub size: Measure<TuiOut>
|
pub size: Measure<TuiOut>
|
||||||
}
|
}
|
||||||
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
||||||
let model = Self::from(Some(phrase.clone()));
|
let model = Self::from(Some(clip.clone()));
|
||||||
model.redraw();
|
model.redraw();
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
||||||
let mut model = Self::default();
|
let mut model = Self::default();
|
||||||
*model.phrase_mut() = phrase;
|
*model.clip_mut() = clip;
|
||||||
model.redraw();
|
model.redraw();
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
|
|
@ -63,33 +63,33 @@ impl TimePoint for MidiEditor {
|
||||||
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
|
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
|
||||||
}
|
}
|
||||||
impl MidiViewer for MidiEditor {
|
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 redraw (&self) { self.mode.redraw() }
|
||||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.phrase() }
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
|
||||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.phrase_mut() }
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
|
||||||
fn set_phrase (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_phrase(p) }
|
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
||||||
}
|
}
|
||||||
impl MidiEditor {
|
impl MidiEditor {
|
||||||
/// Put note at current position
|
/// Put note at current position
|
||||||
pub fn put_note (&mut self, advance: bool) {
|
pub fn put_note (&mut self, advance: bool) {
|
||||||
let mut redraw = false;
|
let mut redraw = false;
|
||||||
if let Some(phrase) = self.phrase() {
|
if let Some(clip) = self.clip() {
|
||||||
let mut phrase = phrase.write().unwrap();
|
let mut clip = clip.write().unwrap();
|
||||||
let note_start = self.time_point();
|
let note_start = self.time_point();
|
||||||
let note_point = self.note_point();
|
let note_point = self.note_point();
|
||||||
let note_len = self.note_len();
|
let note_len = self.note_len();
|
||||||
let note_end = note_start + (note_len.saturating_sub(1));
|
let note_end = note_start + (note_len.saturating_sub(1));
|
||||||
let key: u7 = u7::from(note_point as u8);
|
let key: u7 = u7::from(note_point as u8);
|
||||||
let vel: u7 = 100.into();
|
let vel: u7 = 100.into();
|
||||||
let length = phrase.length;
|
let length = clip.length;
|
||||||
let note_end = note_end % length;
|
let note_end = note_end % length;
|
||||||
let note_on = MidiMessage::NoteOn { key, vel };
|
let note_on = MidiMessage::NoteOn { key, vel };
|
||||||
if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) {
|
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
|
||||||
phrase.notes[note_start].push(note_on);
|
clip.notes[note_start].push(note_on);
|
||||||
}
|
}
|
||||||
let note_off = MidiMessage::NoteOff { key, vel };
|
let note_off = MidiMessage::NoteOff { key, vel };
|
||||||
if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) {
|
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
|
||||||
phrase.notes[note_end].push(note_off);
|
clip.notes[note_end].push(note_off);
|
||||||
}
|
}
|
||||||
if advance {
|
if advance {
|
||||||
self.set_time_point(note_end);
|
self.set_time_point(note_end);
|
||||||
|
|
@ -102,8 +102,8 @@ impl MidiEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
|
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()) {
|
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
||||||
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
|
(clip.color, clip.name.clone(), clip.length, clip.looped)
|
||||||
} else {
|
} else {
|
||||||
(ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false)
|
(ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false)
|
||||||
};
|
};
|
||||||
|
|
@ -114,8 +114,8 @@ impl MidiEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
|
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
|
||||||
let (color, length) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) {
|
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
||||||
(phrase.color, phrase.length)
|
(clip.color, clip.length)
|
||||||
} else {
|
} else {
|
||||||
(ItemPalette::from(TuiTheme::g(64)), 0)
|
(ItemPalette::from(TuiTheme::g(64)), 0)
|
||||||
};
|
};
|
||||||
|
|
@ -140,7 +140,7 @@ impl std::fmt::Debug for MidiEditor {
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum MidiEditCommand {
|
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,
|
AppendNote,
|
||||||
PutNote,
|
PutNote,
|
||||||
SetNoteCursor(usize),
|
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(Char('s')) => SetNoteCursor(s.note_point().saturating_sub(1)),
|
||||||
key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())),
|
key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())),
|
||||||
key(Char('a')) => 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(Up))) => SetNoteScroll(s.note_point() + 3),
|
||||||
ctrl(alt(key(Down))) => SetNoteScroll(s.note_point().saturating_sub(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(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(Up)) => SetNoteScroll(s.note_lo().get() + 1),
|
||||||
ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(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())),
|
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(Up)) => SetNoteCursor(s.note_point() + 3),
|
||||||
alt(key(Down)) => SetNoteCursor(s.note_point().saturating_sub(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(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()),
|
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.phrase_length()),
|
key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()),
|
||||||
key(Char('z')) => SetTimeLock(!s.time_lock().get()),
|
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()) }),
|
||||||
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
|
//// TODO: kpat!(Char('?')) => // toggle dotted
|
||||||
});
|
});
|
||||||
impl MidiEditor {
|
impl MidiEditor {
|
||||||
fn phrase_length (&self) -> usize {
|
fn clip_length (&self) -> usize {
|
||||||
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Command<MidiEditor> for MidiEditCommand {
|
impl Command<MidiEditor> for MidiEditCommand {
|
||||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
||||||
use MidiEditCommand::*;
|
use MidiEditCommand::*;
|
||||||
match self {
|
match self {
|
||||||
Show(phrase) => { state.set_phrase(phrase.as_ref()); },
|
Show(clip) => { state.set_clip(clip.as_ref()); },
|
||||||
PutNote => { state.put_note(false); },
|
PutNote => { state.put_note(false); },
|
||||||
AppendNote => { state.put_note(true); },
|
AppendNote => { state.put_note(true); },
|
||||||
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,10 @@ pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
||||||
if !self.clock().is_rolling() {
|
if !self.clock().is_rolling() {
|
||||||
return
|
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);
|
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();
|
self.record_next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,21 +55,21 @@ pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
||||||
&mut self,
|
&mut self,
|
||||||
scope: &ProcessScope,
|
scope: &ProcessScope,
|
||||||
started: Moment,
|
started: Moment,
|
||||||
phrase: &Option<Arc<RwLock<MidiClip>>>,
|
clip: &Option<Arc<RwLock<MidiClip>>>,
|
||||||
midi_buf: &mut Vec<Vec<Vec<u8>>>
|
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 sample0 = scope.last_frame_time() as usize;
|
||||||
let start = started.sample.get() as usize;
|
let start = started.sample.get() as usize;
|
||||||
let recording = self.recording();
|
let recording = self.recording();
|
||||||
let timebase = self.clock().timebase().clone();
|
let timebase = self.clock().timebase().clone();
|
||||||
let quant = self.clock().quant.get();
|
let quant = self.clock().quant.get();
|
||||||
let mut phrase = phrase.write().unwrap();
|
let mut clip = clip.write().unwrap();
|
||||||
let length = phrase.length;
|
let length = clip.length;
|
||||||
for input in self.midi_ins_mut().iter() {
|
for input in self.midi_ins_mut().iter() {
|
||||||
for (sample, event, bytes) in parse_midi_input(input.port.iter(scope)) {
|
for (sample, event, bytes) in parse_midi_input(input.port.iter(scope)) {
|
||||||
if let LiveEvent::Midi { message, .. } = event {
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
phrase.record_event({
|
clip.record_event({
|
||||||
let sample = (sample0 + sample - start) as f64;
|
let sample = (sample0 + sample - start) as f64;
|
||||||
let pulse = timebase.samples_to_pulse(sample);
|
let pulse = timebase.samples_to_pulse(sample);
|
||||||
let quantized = (pulse / quant).round() * quant;
|
let quantized = (pulse / quant).round() * quant;
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ use crate::*;
|
||||||
pub trait HasPlayPhrase: HasClock {
|
pub trait HasPlayPhrase: HasClock {
|
||||||
fn reset (&self) -> bool;
|
fn reset (&self) -> bool;
|
||||||
fn reset_mut (&mut self) -> &mut bool;
|
fn reset_mut (&mut self) -> &mut bool;
|
||||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||||
fn next_phrase_mut (&mut self) -> &mut 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> {
|
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();
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||||
Some(elapsed)
|
Some(elapsed)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -16,9 +16,9 @@ pub trait HasPlayPhrase: HasClock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
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 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 times = (elapsed as usize / length) as f64;
|
||||||
let elapsed = (elapsed as usize % length) as f64;
|
let elapsed = (elapsed as usize % length) as f64;
|
||||||
Some((times, elapsed))
|
Some((times, elapsed))
|
||||||
|
|
@ -26,10 +26,10 @@ pub trait HasPlayPhrase: HasClock {
|
||||||
None
|
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 start = self.clock().next_launch_pulse() as f64;
|
||||||
let instant = Moment::from_pulse(self.clock().timebase(), start);
|
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;
|
*self.reset_mut() = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
fn play (
|
||||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if !self.clock().is_rolling() {
|
if !self.clock().is_rolling() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// If a phrase is playing, write a chunk of MIDI events from it to the output buffer.
|
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
|
||||||
// If no phrase is playing, prepare for switchover immediately.
|
// If no clip is playing, prepare for switchover immediately.
|
||||||
self.play_phrase().as_ref().map_or(true, |(started, phrase)|{
|
self.play_clip().as_ref().map_or(true, |(started, clip)|{
|
||||||
self.play_chunk(scope, note_buf, out, started, phrase)
|
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 (
|
fn switchover (
|
||||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
&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 sample0 = scope.last_frame_time() as usize;
|
||||||
//let samples = scope.n_frames() 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 start = start_at.sample.get() as usize;
|
||||||
let sample = self.clock().started.read().unwrap()
|
let sample = self.clock().started.read().unwrap()
|
||||||
.as_ref().unwrap().sample.get() as usize;
|
.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) {
|
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;
|
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);
|
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
||||||
// Launch enqueued phrase
|
// Launch enqueued clip
|
||||||
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
*self.play_clip_mut() = Some((started, clip.clone()));
|
||||||
// Unset enqueuement (TODO: where to implement looping?)
|
// Unset enqueuement (TODO: where to implement looping?)
|
||||||
*self.next_phrase_mut() = None;
|
*self.next_clip_mut() = None;
|
||||||
// Fill in remaining ticks of chunk from next phrase.
|
// Fill in remaining ticks of chunk from next clip.
|
||||||
self.play(scope, note_buf, out);
|
self.play(scope, note_buf, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,52 +77,52 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||||
note_buf: &mut Vec<u8>,
|
note_buf: &mut Vec<u8>,
|
||||||
out: &mut [Vec<Vec<u8>>],
|
out: &mut [Vec<Vec<u8>>],
|
||||||
started: &Moment,
|
started: &Moment,
|
||||||
phrase: &Option<Arc<RwLock<MidiClip>>>
|
clip: &Option<Arc<RwLock<MidiClip>>>
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// First sample to populate. Greater than 0 means that the first
|
// 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(
|
let sample = (scope.last_frame_time() as usize).saturating_sub(
|
||||||
started.sample.get() as usize +
|
started.sample.get() as usize +
|
||||||
self.clock().started.read().unwrap().as_ref().unwrap().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)
|
// 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.
|
// 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);
|
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
|
||||||
// Notes active during current chunk.
|
// Notes active during current chunk.
|
||||||
let notes = &mut self.notes_out().write().unwrap();
|
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 {
|
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)
|
// break the loop here (FIXME count pulse correctly)
|
||||||
let past_end = if phrase.is_some() { pulse >= length } else { true };
|
let past_end = if clip.is_some() { pulse >= length } else { true };
|
||||||
if self.next_phrase().is_some() && past_end {
|
if self.next_clip().is_some() && past_end {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// If there's a currently playing phrase, output notes from it to buffer:
|
// If there's a currently playing clip, output notes from it to buffer:
|
||||||
if let Some(ref phrase) = phrase {
|
if let Some(ref clip) = clip {
|
||||||
Self::play_pulse(phrase, pulse, sample, note_buf, out, notes)
|
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play_pulse (
|
fn play_pulse (
|
||||||
phrase: &RwLock<MidiClip>,
|
clip: &RwLock<MidiClip>,
|
||||||
pulse: usize,
|
pulse: usize,
|
||||||
sample: usize,
|
sample: usize,
|
||||||
note_buf: &mut Vec<u8>,
|
note_buf: &mut Vec<u8>,
|
||||||
out: &mut [Vec<Vec<u8>>],
|
out: &mut [Vec<Vec<u8>>],
|
||||||
notes: &mut [bool;128]
|
notes: &mut [bool;128]
|
||||||
) {
|
) {
|
||||||
// Source phrase from which the MIDI events will be taken.
|
// Source clip from which the MIDI events will be taken.
|
||||||
let phrase = phrase.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
// Phrase with zero length is not processed
|
// Phrase with zero length is not processed
|
||||||
if phrase.length > 0 {
|
if clip.length > 0 {
|
||||||
// Current pulse index in source phrase
|
// Current pulse index in source clip
|
||||||
let pulse = pulse % phrase.length;
|
let pulse = pulse % clip.length;
|
||||||
// Output each MIDI event from phrase at appropriate frames of output buffer:
|
// Output each MIDI event from clip at appropriate frames of output buffer:
|
||||||
for message in phrase.notes[pulse].iter() {
|
for message in clip.notes[pulse].iter() {
|
||||||
// Clear output buffer for this MIDI event.
|
// Clear output buffer for this MIDI event.
|
||||||
note_buf.clear();
|
note_buf.clear();
|
||||||
// TODO: support MIDI channels other than CH1.
|
// TODO: support MIDI channels other than CH1.
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,14 @@ pub trait HasPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains state for playing a phrase
|
/// Contains state for playing a clip
|
||||||
pub struct MidiPlayer {
|
pub struct MidiPlayer {
|
||||||
/// State of clock and playhead
|
/// State of clock and playhead
|
||||||
pub clock: Clock,
|
pub clock: Clock,
|
||||||
/// Start time and phrase being played
|
/// Start time and clip being played
|
||||||
pub play_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
/// Start time and next phrase
|
/// Start time and next clip
|
||||||
pub next_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
/// Play input through output.
|
/// Play input through output.
|
||||||
pub monitoring: bool,
|
pub monitoring: bool,
|
||||||
/// Write input to sequence.
|
/// Write input to sequence.
|
||||||
|
|
@ -56,8 +56,8 @@ impl MidiPlayer {
|
||||||
let name = name.as_ref();
|
let name = name.as_ref();
|
||||||
let clock = Clock::from(jack);
|
let clock = Clock::from(jack);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
play_phrase: Some((Moment::zero(&clock.timebase), clip.cloned())),
|
play_clip: Some((Moment::zero(&clock.timebase), clip.cloned())),
|
||||||
next_phrase: None,
|
next_clip: None,
|
||||||
recording: false,
|
recording: false,
|
||||||
monitoring: false,
|
monitoring: false,
|
||||||
overdub: false,
|
overdub: false,
|
||||||
|
|
@ -73,18 +73,18 @@ impl MidiPlayer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn play_status (&self) -> impl Content<TuiOut> {
|
pub fn play_status (&self) -> impl Content<TuiOut> {
|
||||||
ClipSelected::play_phrase(self)
|
ClipSelected::play_clip(self)
|
||||||
}
|
}
|
||||||
pub fn next_status (&self) -> impl Content<TuiOut> {
|
pub fn next_status (&self) -> impl Content<TuiOut> {
|
||||||
ClipSelected::next_phrase(self)
|
ClipSelected::next_clip(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl std::fmt::Debug for MidiPlayer {
|
impl std::fmt::Debug for MidiPlayer {
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
f.debug_struct("MidiPlayer")
|
f.debug_struct("MidiPlayer")
|
||||||
.field("clock", &self.clock)
|
.field("clock", &self.clock)
|
||||||
.field("play_phrase", &self.play_phrase)
|
.field("play_clip", &self.play_clip)
|
||||||
.field("next_phrase", &self.next_phrase)
|
.field("next_clip", &self.next_clip)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,15 +97,15 @@ from!(|clock: &Clock| MidiPlayer = Self {
|
||||||
recording: false,
|
recording: false,
|
||||||
monitoring: false,
|
monitoring: false,
|
||||||
overdub: false,
|
overdub: false,
|
||||||
play_phrase: None,
|
play_clip: None,
|
||||||
next_phrase: None,
|
next_clip: None,
|
||||||
notes_in: RwLock::new([false;128]).into(),
|
notes_in: RwLock::new([false;128]).into(),
|
||||||
notes_out: RwLock::new([false;128]).into(),
|
notes_out: RwLock::new([false;128]).into(),
|
||||||
});
|
});
|
||||||
from!(|state: (&Clock, &Arc<RwLock<MidiClip>>)|MidiPlayer = {
|
from!(|state: (&Clock, &Arc<RwLock<MidiClip>>)|MidiPlayer = {
|
||||||
let (clock, phrase) = state;
|
let (clock, clip) = state;
|
||||||
let mut model = Self::from(clock);
|
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
|
model
|
||||||
});
|
});
|
||||||
has_clock!(|self: MidiPlayer|&self.clock);
|
has_clock!(|self: MidiPlayer|&self.clock);
|
||||||
|
|
@ -129,7 +129,7 @@ pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
||||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
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> {
|
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
let model = &mut self.0;
|
let model = &mut self.0;
|
||||||
|
|
@ -137,7 +137,7 @@ impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
|
||||||
let midi_buf = &mut self.2;
|
let midi_buf = &mut self.2;
|
||||||
// Clear output buffer(s)
|
// Clear output buffer(s)
|
||||||
model.clear(scope, midi_buf, false);
|
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) {
|
if model.play(scope, note_buf, midi_buf) {
|
||||||
model.switchover(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 {
|
fn reset_mut (&mut self) -> &mut bool {
|
||||||
&mut self.reset
|
&mut self.reset
|
||||||
}
|
}
|
||||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
&self.play_phrase
|
&self.play_clip
|
||||||
}
|
}
|
||||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
&mut self.play_phrase
|
&mut self.play_clip
|
||||||
}
|
}
|
||||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
&self.next_phrase
|
&self.next_clip
|
||||||
}
|
}
|
||||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
&mut self.next_phrase
|
&mut self.next_clip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,10 +211,10 @@ impl HasPlayPhrase for MidiPlayer {
|
||||||
//pub struct MIDIPlayer {
|
//pub struct MIDIPlayer {
|
||||||
///// Global timebase
|
///// Global timebase
|
||||||
//pub clock: Arc<Clock>,
|
//pub clock: Arc<Clock>,
|
||||||
///// Start time and phrase being played
|
///// Start time and clip being played
|
||||||
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
//pub play_clip: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
///// Start time and next phrase
|
///// Start time and next clip
|
||||||
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
//pub next_clip: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
///// Play input through output.
|
///// Play input through output.
|
||||||
//pub monitoring: bool,
|
//pub monitoring: bool,
|
||||||
///// Write input to sequence.
|
///// Write input to sequence.
|
||||||
|
|
@ -247,8 +247,8 @@ impl HasPlayPhrase for MidiPlayer {
|
||||||
//let jack = jack.read().unwrap();
|
//let jack = jack.read().unwrap();
|
||||||
//Ok(Self {
|
//Ok(Self {
|
||||||
//clock: clock.clone(),
|
//clock: clock.clone(),
|
||||||
//phrase: None,
|
//clip: None,
|
||||||
//next_phrase: None,
|
//next_clip: None,
|
||||||
//notes_in: Arc::new(RwLock::new([false;128])),
|
//notes_in: Arc::new(RwLock::new([false;128])),
|
||||||
//notes_out: Arc::new(RwLock::new([false;128])),
|
//notes_out: Arc::new(RwLock::new([false;128])),
|
||||||
//monitoring: false,
|
//monitoring: false,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait HasPhrases {
|
pub trait HasPhrases {
|
||||||
fn phrases (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
|
fn clips (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
|
||||||
fn phrases_mut (&mut self) -> &mut 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) => {
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? {
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? {
|
||||||
fn phrases (&$self) -> &Vec<Arc<RwLock<MidiClip>>> { &$cb }
|
fn clips (&$self) -> &Vec<Arc<RwLock<MidiClip>>> { &$cb }
|
||||||
fn phrases_mut (&mut $self) -> &mut Vec<Arc<RwLock<MidiClip>>> { &mut$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> {
|
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||||
use MidiPoolCommand::*;
|
use MidiPoolCommand::*;
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Add(mut index, phrase) => {
|
Add(mut index, clip) => {
|
||||||
let phrase = Arc::new(RwLock::new(phrase));
|
let clip = Arc::new(RwLock::new(clip));
|
||||||
let phrases = model.phrases_mut();
|
let clips = model.clips_mut();
|
||||||
if index >= phrases.len() {
|
if index >= clips.len() {
|
||||||
index = phrases.len();
|
index = clips.len();
|
||||||
phrases.push(phrase)
|
clips.push(clip)
|
||||||
} else {
|
} else {
|
||||||
phrases.insert(index, phrase);
|
clips.insert(index, clip);
|
||||||
}
|
}
|
||||||
Some(Self::Delete(index))
|
Some(Self::Delete(index))
|
||||||
},
|
},
|
||||||
Delete(index) => {
|
Delete(index) => {
|
||||||
let phrase = model.phrases_mut().remove(index).read().unwrap().clone();
|
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
||||||
Some(Self::Add(index, phrase))
|
Some(Self::Add(index, clip))
|
||||||
},
|
},
|
||||||
Swap(index, other) => {
|
Swap(index, other) => {
|
||||||
model.phrases_mut().swap(index, other);
|
model.clips_mut().swap(index, other);
|
||||||
Some(Self::Swap(index, other))
|
Some(Self::Swap(index, other))
|
||||||
},
|
},
|
||||||
Import(index, path) => {
|
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() {
|
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) => {
|
Export(_index, _path) => {
|
||||||
todo!("export phrase to midi file");
|
todo!("export clip to midi file");
|
||||||
},
|
},
|
||||||
SetName(index, name) => {
|
SetName(index, name) => {
|
||||||
let mut phrase = model.phrases()[index].write().unwrap();
|
let mut clip = model.clips()[index].write().unwrap();
|
||||||
let old_name = phrase.name.clone();
|
let old_name = clip.name.clone();
|
||||||
phrase.name = name;
|
clip.name = name;
|
||||||
Some(Self::SetName(index, old_name))
|
Some(Self::SetName(index, old_name))
|
||||||
},
|
},
|
||||||
SetLength(index, length) => {
|
SetLength(index, length) => {
|
||||||
let mut phrase = model.phrases()[index].write().unwrap();
|
let mut clip = model.clips()[index].write().unwrap();
|
||||||
let old_len = phrase.length;
|
let old_len = clip.length;
|
||||||
phrase.length = length;
|
clip.length = length;
|
||||||
Some(Self::SetLength(index, old_len))
|
Some(Self::SetLength(index, old_len))
|
||||||
},
|
},
|
||||||
SetColor(index, color) => {
|
SetColor(index, color) => {
|
||||||
let mut color = ItemPalette::from(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))
|
Some(Self::SetColor(index, color.base))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ render!(TuiOut: (self: ClipSelected) =>
|
||||||
|
|
||||||
impl ClipSelected {
|
impl ClipSelected {
|
||||||
|
|
||||||
/// Shows currently playing phrase with beats elapsed
|
/// Shows currently playing clip with beats elapsed
|
||||||
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
pub fn play_clip <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
||||||
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
|
let (name, color) = if let Some((_, Some(clip))) = state.play_clip() {
|
||||||
let MidiClip { ref name, color, .. } = *phrase.read().unwrap();
|
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
||||||
(name.clone().into(), color)
|
(name.clone().into(), color)
|
||||||
} else {
|
} else {
|
||||||
("".to_string().into(), TuiTheme::g(64).into())
|
("".to_string().into(), TuiTheme::g(64).into())
|
||||||
|
|
@ -32,15 +32,15 @@ impl ClipSelected {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows next phrase with beats remaining until switchover
|
/// Shows next clip with beats remaining until switchover
|
||||||
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
|
pub fn next_clip <T: HasPlayPhrase> (state: &T) -> Self {
|
||||||
let mut time: Arc<str> = String::from("--.-.--").into();
|
let mut time: Arc<str> = String::from("--.-.--").into();
|
||||||
let mut name: Arc<str> = String::from("").into();
|
let mut name: Arc<str> = String::from("").into();
|
||||||
let mut color = ItemPalette::from(TuiTheme::g(64));
|
let mut color = ItemPalette::from(TuiTheme::g(64));
|
||||||
if let Some((t, Some(phrase))) = state.next_phrase() {
|
if let Some((t, Some(clip))) = state.next_clip() {
|
||||||
let phrase = phrase.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
name = phrase.name.clone();
|
name = clip.name.clone();
|
||||||
color = phrase.color.clone();
|
color = clip.color.clone();
|
||||||
time = {
|
time = {
|
||||||
let target = t.pulse.get();
|
let target = t.pulse.get();
|
||||||
let current = state.clock().playhead.pulse.get();
|
let current = state.clock().playhead.pulse.get();
|
||||||
|
|
@ -51,12 +51,12 @@ impl ClipSelected {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
}.into()
|
}.into()
|
||||||
} else if let Some((t, Some(phrase))) = state.play_phrase() {
|
} else if let Some((t, Some(clip))) = state.play_clip() {
|
||||||
let phrase = phrase.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
if phrase.looped {
|
if clip.looped {
|
||||||
name = phrase.name.clone();
|
name = clip.name.clone();
|
||||||
color = phrase.color.clone();
|
color = clip.color.clone();
|
||||||
let target = t.pulse.get() + phrase.length as f64;
|
let target = t.pulse.get() + clip.length as f64;
|
||||||
let current = state.clock().playhead.pulse.get();
|
let current = state.clock().playhead.pulse.get();
|
||||||
if target > current {
|
if target > current {
|
||||||
time = format!("-{:>}", state.clock().timebase.format_beats_0(
|
time = format!("-{:>}", state.clock().timebase.format_beats_0(
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait MidiViewer: HasSize<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
|
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 redraw (&self);
|
||||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
||||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
||||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||||
*self.phrase_mut() = phrase.cloned();
|
*self.clip_mut() = clip.cloned();
|
||||||
self.redraw();
|
self.redraw();
|
||||||
}
|
}
|
||||||
/// Make sure cursor is within note range
|
/// Make sure cursor is within note range
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,42 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use super::*;
|
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)> {
|
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))
|
(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(
|
Fill::xy(Bsp::e(
|
||||||
Fixed::x(self.keys_width, PianoHorizontalKeys(self)),
|
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 {
|
impl PianoHorizontal {
|
||||||
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
/// 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 (y, note) in (0..=127).rev().enumerate() {
|
||||||
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
||||||
let cell = buf.get_mut(x, y).unwrap();
|
let cell = buf.get_mut(x, y).unwrap();
|
||||||
cell.set_bg(phrase.color.darkest.rgb);
|
cell.set_bg(clip.color.darkest.rgb);
|
||||||
cell.set_fg(phrase.color.darker.rgb);
|
cell.set_fg(clip.color.darker.rgb);
|
||||||
cell.set_char(if time % 384 == 0 {
|
cell.set_char(if time % 384 == 0 {
|
||||||
'│'
|
'│'
|
||||||
} else if time % 96 == 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: █▄ █▄ █▄
|
/// 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) {
|
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
|
||||||
let style = Style::default().fg(phrase.color.base.rgb);//.bg(Color::Rgb(0, 0, 0));
|
let style = Style::default().fg(clip.color.base.rgb);//.bg(Color::Rgb(0, 0, 0));
|
||||||
let mut notes_on = [false;128];
|
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() {
|
for (y, note) in (0..=127).rev().enumerate() {
|
||||||
if let Some(cell) = buf.get_mut(x, note) {
|
if let Some(cell) = buf.get_mut(x, note) {
|
||||||
|
|
@ -53,8 +95,8 @@ impl PianoHorizontal {
|
||||||
}
|
}
|
||||||
|
|
||||||
let time_end = time_start + zoom;
|
let time_end = time_start + zoom;
|
||||||
for time in time_start..time_end.min(phrase.length) {
|
for time in time_start..time_end.min(clip.length) {
|
||||||
for event in phrase.notes[time].iter() {
|
for event in clip.notes[time].iter() {
|
||||||
match event {
|
match event {
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
let note = key.as_int() as usize;
|
let note = key.as_int() as usize;
|
||||||
|
|
@ -80,31 +122,32 @@ impl PianoHorizontal {
|
||||||
let note_hi = self.note_hi();
|
let note_hi = self.note_hi();
|
||||||
let note_point = self.note_point();
|
let note_point = self.note_point();
|
||||||
let buffer = self.buffer.clone();
|
let buffer = self.buffer.clone();
|
||||||
RenderThunk::new(move|render: &mut TuiOut|{
|
return ""
|
||||||
let source = buffer.read().unwrap();
|
//RenderThunk::new(move|render: &mut TuiOut|{
|
||||||
let [x0, y0, w, h] = render.area().xywh();
|
//let source = buffer.read().unwrap();
|
||||||
if h as usize != note_axis {
|
//let [x0, y0, w, h] = render.area().xywh();
|
||||||
panic!("area height mismatch: {h} <> {note_axis}");
|
//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) {
|
//for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
||||||
let source_x = time_start + area_x;
|
//for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
let source_y = note_hi - area_y;
|
//let source_x = time_start + area_x;
|
||||||
// TODO: enable loop rollover:
|
//let source_y = note_hi - area_y;
|
||||||
//let source_x = (time_start + area_x) % source.width.max(1);
|
//// TODO: enable loop rollover:
|
||||||
//let source_y = (note_hi - area_y) % source.height.max(1);
|
////let source_x = (time_start + area_x) % source.width.max(1);
|
||||||
let is_in_x = source_x < source.width;
|
////let source_y = (note_hi - area_y) % source.height.max(1);
|
||||||
let is_in_y = source_y < source.height;
|
//let is_in_x = source_x < source.width;
|
||||||
if is_in_x && is_in_y {
|
//let is_in_y = source_y < source.height;
|
||||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
//if is_in_x && is_in_y {
|
||||||
if let Some(cell) = render.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
|
//if let Some(source_cell) = source.get(source_x, source_y) {
|
||||||
*cell = source_cell.clone();
|
//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> {
|
fn cursor (&self) -> impl Content<TuiOut> {
|
||||||
let style = Some(Style::default().fg(self.color.lightest.rgb));
|
let style = Some(Style::default().fg(self.color.lightest.rgb));
|
||||||
|
|
@ -115,27 +158,28 @@ impl PianoHorizontal {
|
||||||
let time_point = self.time_point();
|
let time_point = self.time_point();
|
||||||
let time_start = self.time_start().get();
|
let time_start = self.time_start().get();
|
||||||
let time_zoom = self.time_zoom().get();
|
let time_zoom = self.time_zoom().get();
|
||||||
RenderThunk::new(move|render: &mut TuiOut|{
|
""
|
||||||
let [x0, y0, w, _] = render.area().xywh();
|
//RenderThunk::new(move|render: &mut TuiOut|{
|
||||||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
////let [x0, y0, w, _] = render.area().xywh();
|
||||||
if note == note_point {
|
////for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
for x in 0..w {
|
////if note == note_point {
|
||||||
let screen_x = x0 + x;
|
////for x in 0..w {
|
||||||
let time_1 = time_start + x as usize * time_zoom;
|
////let screen_x = x0 + x;
|
||||||
let time_2 = time_1 + time_zoom;
|
////let time_1 = time_start + x as usize * time_zoom;
|
||||||
if time_1 <= time_point && time_point < time_2 {
|
////let time_2 = time_1 + time_zoom;
|
||||||
render.blit(&"█", screen_x, screen_y, style);
|
////if time_1 <= time_point && time_point < time_2 {
|
||||||
let tail = note_len as u16 / time_zoom as u16;
|
////render.blit(&"█", screen_x, screen_y, style);
|
||||||
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
////let tail = note_len as u16 / time_zoom as u16;
|
||||||
render.blit(&"▂", x_tail, screen_y, style);
|
////for x_tail in (screen_x + 1)..(screen_x + tail) {
|
||||||
}
|
////render.blit(&"▂", x_tail, screen_y, style);
|
||||||
break
|
////}
|
||||||
}
|
////break
|
||||||
}
|
////}
|
||||||
break
|
////}
|
||||||
}
|
////break
|
||||||
}
|
////}
|
||||||
})
|
////}
|
||||||
|
//})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,35 +207,35 @@ impl TimePoint for PianoHorizontal {
|
||||||
fn set_time_point (&self, x: usize) { self.point.set_time_point(x) }
|
fn set_time_point (&self, x: usize) { self.point.set_time_point(x) }
|
||||||
}
|
}
|
||||||
impl MidiViewer for PianoHorizontal {
|
impl MidiViewer for PianoHorizontal {
|
||||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
||||||
&self.phrase
|
&self.clip
|
||||||
}
|
}
|
||||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||||
&mut self.phrase
|
&mut self.clip
|
||||||
}
|
}
|
||||||
/// Determine the required space to render the phrase.
|
/// Determine the required space to render the clip.
|
||||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
|
||||||
(phrase.length / self.range.time_zoom().get(), 128)
|
(clip.length / self.range.time_zoom().get(), 128)
|
||||||
}
|
}
|
||||||
fn redraw (&self) {
|
fn redraw (&self) {
|
||||||
let buffer = if let Some(phrase) = self.phrase.as_ref() {
|
let buffer = if let Some(clip) = self.clip.as_ref() {
|
||||||
let phrase = phrase.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
let buf_size = self.buffer_size(&phrase);
|
let buf_size = self.buffer_size(&clip);
|
||||||
let mut buffer = BigBuffer::from(buf_size);
|
let mut buffer = BigBuffer::from(buf_size);
|
||||||
let note_len = self.note_len();
|
let note_len = self.note_len();
|
||||||
let time_zoom = self.time_zoom().get();
|
let time_zoom = self.time_zoom().get();
|
||||||
self.time_len().set(phrase.length);
|
self.time_len().set(clip.length);
|
||||||
PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len);
|
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
|
||||||
PianoHorizontal::draw_fg(&mut buffer, &phrase, time_zoom);
|
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
|
||||||
buffer
|
buffer
|
||||||
} else {
|
} else {
|
||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
*self.buffer.write().unwrap() = buffer
|
*self.buffer.write().unwrap() = buffer
|
||||||
}
|
}
|
||||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||||
*self.phrase_mut() = phrase.cloned();
|
*self.clip_mut() = clip.cloned();
|
||||||
self.color = phrase.map(|p|p.read().unwrap().color)
|
self.color = clip.map(|p|p.read().unwrap().color)
|
||||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||||
self.redraw();
|
self.redraw();
|
||||||
}
|
}
|
||||||
|
|
@ -208,12 +252,12 @@ impl std::fmt::Debug for PianoHorizontal {
|
||||||
}
|
}
|
||||||
// Update sequencer playhead indicator
|
// Update sequencer playhead indicator
|
||||||
//self.now().set(0.);
|
//self.now().set(0.);
|
||||||
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
|
//if let Some((ref started_at, Some(ref playing))) = self.player.play_clip {
|
||||||
//let phrase = phrase.read().unwrap();
|
//let clip = clip.read().unwrap();
|
||||||
//if *playing.read().unwrap() == *phrase {
|
//if *playing.read().unwrap() == *clip {
|
||||||
//let pulse = self.current().pulse.get();
|
//let pulse = self.current().pulse.get();
|
||||||
//let start = started_at.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);
|
//self.now().set(now);
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ pub struct PianoHorizontalTimeline<'a>(pub(crate) &'a PianoHorizontal);
|
||||||
render!(TuiOut: |self: PianoHorizontalTimeline<'a>, render|{
|
render!(TuiOut: |self: PianoHorizontalTimeline<'a>, render|{
|
||||||
let [x, y, w, h] = render.area();
|
let [x, y, w, h] = render.area();
|
||||||
let style = Some(Style::default().dim());
|
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)) {
|
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
||||||
let t = area_x as usize * self.0.time_zoom().get();
|
let t = area_x as usize * self.0.time_zoom().get();
|
||||||
if t < length {
|
if t < length {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ impl<E: Output> Measure<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn of <T: Content<E>> (&self, item: T) -> Bsp<Fill<&Self>, T> {
|
pub fn of <T: Content<E>> (&self, item: T) -> Bsp<Fill<&Self>, T> {
|
||||||
Bsp::b(Fill::xy(&self), item)
|
Bsp::b(Fill::xy(self), item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,15 @@ audio!(|self: Arranger, client, scope|{
|
||||||
// FIXME: one of these per playing track
|
// FIXME: one of these per playing track
|
||||||
//self.now.set(0.);
|
//self.now.set(0.);
|
||||||
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||||
//let phrase = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
//let clip = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
||||||
//if let Some(Some(Some(phrase))) = phrase {
|
//if let Some(Some(Some(clip))) = clip {
|
||||||
//if let Some(track) = self.tracks().get(t) {
|
//if let Some(track) = self.tracks().get(t) {
|
||||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
|
//if let Some((ref started_at, Some(ref playing))) = track.player.play_clip {
|
||||||
//let phrase = phrase.read().unwrap();
|
//let clip = clip.read().unwrap();
|
||||||
//if *playing.read().unwrap() == *phrase {
|
//if *playing.read().unwrap() == *clip {
|
||||||
//let pulse = self.current().pulse.get();
|
//let pulse = self.current().pulse.get();
|
||||||
//let start = started_at.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);
|
//self.now.set(now);
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
@ -62,7 +62,7 @@ audio!(|self: Arranger, client, scope|{
|
||||||
return Control::Continue
|
return Control::Continue
|
||||||
});
|
});
|
||||||
has_clock!(|self: Arranger|&self.clock);
|
has_clock!(|self: Arranger|&self.clock);
|
||||||
has_phrases!(|self: Arranger|self.pool.phrases);
|
has_clips!(|self: Arranger|self.pool.clips);
|
||||||
has_editor!(|self: Arranger|self.editor);
|
has_editor!(|self: Arranger|self.editor);
|
||||||
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
|
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
|
||||||
impl Arranger {
|
impl Arranger {
|
||||||
|
|
@ -75,26 +75,26 @@ impl Arranger {
|
||||||
pub fn activate (&mut self) -> Usually<()> {
|
pub fn activate (&mut self) -> Usually<()> {
|
||||||
if let ArrangerSelection::Scene(s) = self.selected {
|
if let ArrangerSelection::Scene(s) = self.selected {
|
||||||
for (t, track) in self.tracks.iter_mut().enumerate() {
|
for (t, track) in self.tracks.iter_mut().enumerate() {
|
||||||
let phrase = self.scenes[s].clips[t].clone();
|
let clip = self.scenes[s].clips[t].clone();
|
||||||
if track.player.play_phrase.is_some() || phrase.is_some() {
|
if track.player.play_clip.is_some() || clip.is_some() {
|
||||||
track.player.enqueue_next(phrase.as_ref());
|
track.player.enqueue_next(clip.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.clock().is_stopped() {
|
if self.clock().is_stopped() {
|
||||||
self.clock().play_from(Some(0))?;
|
self.clock().play_from(Some(0))?;
|
||||||
}
|
}
|
||||||
} else if let ArrangerSelection::Clip(t, s) = self.selected {
|
} else if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||||
let phrase = self.scenes[s].clips[t].clone();
|
let clip = self.scenes[s].clips[t].clone();
|
||||||
self.tracks[t].player.enqueue_next(phrase.as_ref());
|
self.tracks[t].player.enqueue_next(clip.as_ref());
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn selected_phrase (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
pub fn selected_clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
||||||
}
|
}
|
||||||
pub fn toggle_loop (&mut self) {
|
pub fn toggle_loop (&mut self) {
|
||||||
if let Some(phrase) = self.selected_phrase() {
|
if let Some(clip) = self.selected_clip() {
|
||||||
phrase.write().unwrap().toggle_loop()
|
clip.write().unwrap().toggle_loop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn randomize_color (&mut self) {
|
pub fn randomize_color (&mut self) {
|
||||||
|
|
@ -102,8 +102,8 @@ impl Arranger {
|
||||||
ArrangerSelection::Mix => { self.color = ItemPalette::random() },
|
ArrangerSelection::Mix => { self.color = ItemPalette::random() },
|
||||||
ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
||||||
ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
||||||
ArrangerSelection::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
|
ArrangerSelection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
|
||||||
phrase.write().unwrap().color = ItemPalette::random();
|
clip.write().unwrap().color = ItemPalette::random();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,10 @@ keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
||||||
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
||||||
// Transport: Play from start or rewind to start
|
// Transport: Play from start or rewind to start
|
||||||
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
||||||
key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(Some(state.pool.phrase().clone()))),
|
key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(Some(state.pool.clip().clone()))),
|
||||||
ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add),
|
ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add),
|
||||||
ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add),
|
ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add),
|
||||||
// Tab: Toggle visibility of phrase pool column
|
// Tab: Toggle visibility of clip pool column
|
||||||
key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)),
|
key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)),
|
||||||
}, {
|
}, {
|
||||||
use ArrangerSelection as Selected;
|
use ArrangerSelection as Selected;
|
||||||
|
|
@ -81,7 +81,7 @@ keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
||||||
kpat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
kpat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||||
kpat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
kpat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||||
kpat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
kpat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||||
kpat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.phrase().clone())))),
|
kpat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.clip().clone())))),
|
||||||
kpat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
|
kpat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
|
||||||
kpat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
kpat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||||
|
|
||||||
|
|
@ -178,16 +178,16 @@ command!(|self: ArrangerCommand, state: Arranger|match self {
|
||||||
},
|
},
|
||||||
Self::Phrases(cmd) => {
|
Self::Phrases(cmd) => {
|
||||||
match cmd {
|
match cmd {
|
||||||
// autoselect: automatically load selected phrase in editor
|
// autoselect: automatically load selected clip in editor
|
||||||
PoolCommand::Select(_) => {
|
PoolCommand::Select(_) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
// reload phrase in editor to update color
|
// reload clip in editor to update color
|
||||||
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
|
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Phrases)?
|
_ => cmd.delegate(&mut state.pool, Self::Phrases)?
|
||||||
|
|
@ -234,9 +234,9 @@ command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
||||||
});
|
});
|
||||||
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
||||||
Self::Get(track, scene) => { todo!() },
|
Self::Get(track, scene) => { todo!() },
|
||||||
Self::Put(track, scene, phrase) => {
|
Self::Put(track, scene, clip) => {
|
||||||
let old = state.scenes[scene].clips[track].clone();
|
let old = state.scenes[scene].clips[track].clone();
|
||||||
state.scenes[scene].clips[track] = phrase;
|
state.scenes[scene].clips[track] = clip;
|
||||||
Some(Self::Put(track, scene, old))
|
Some(Self::Put(track, scene, old))
|
||||||
},
|
},
|
||||||
Self::Enqueue(track, scene) => {
|
Self::Enqueue(track, scene) => {
|
||||||
|
|
|
||||||
|
|
@ -56,22 +56,22 @@ impl ArrangerScene {
|
||||||
pub fn longest_name (scenes: &[Self]) -> usize {
|
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||||
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||||
}
|
}
|
||||||
/// Returns the pulse length of the longest phrase in the scene
|
/// Returns the pulse length of the longest clip in the scene
|
||||||
pub fn pulses (&self) -> usize {
|
pub fn pulses (&self) -> usize {
|
||||||
self.clips().iter().fold(0, |a, p|{
|
self.clips().iter().fold(0, |a, p|{
|
||||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/// Returns true if all phrases in the scene are
|
/// Returns true if all clips in the scene are
|
||||||
/// currently playing on the given collection of tracks.
|
/// currently playing on the given collection of tracks.
|
||||||
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
||||||
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
|
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
|
||||||
.all(|(track_index, clip)|match clip {
|
.all(|(track_index, clip)|match clip {
|
||||||
Some(clip) => tracks
|
Some(c) => tracks
|
||||||
.get(track_index)
|
.get(track_index)
|
||||||
.map(|track|{
|
.map(|track|{
|
||||||
if let Some((_, Some(phrase))) = track.player().play_phrase() {
|
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
*clip.read().unwrap() == *c.read().unwrap()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ impl Arranger {
|
||||||
let color: ItemPalette = track.color().dark.into();
|
let color: ItemPalette = track.color().dark.into();
|
||||||
let timebase = self.clock().timebase();
|
let timebase = self.clock().timebase();
|
||||||
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
||||||
if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
||||||
let length = phrase.read().unwrap().length;
|
let length = clip.read().unwrap().length;
|
||||||
let elapsed = track.player.pulses_since_start().unwrap() as usize;
|
let elapsed = track.player.pulses_since_start().unwrap() as usize;
|
||||||
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
|
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -229,7 +229,7 @@ impl Arranger {
|
||||||
{
|
{
|
||||||
let timebase = ¤t.timebase;
|
let timebase = ¤t.timebase;
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
if let Some((t, _)) = track.player.next_phrase().as_ref() {
|
if let Some((t, _)) = track.player.next_clip().as_ref() {
|
||||||
let target = t.pulse.get();
|
let target = t.pulse.get();
|
||||||
let current = current.pulse.get();
|
let current = current.pulse.get();
|
||||||
if target > current {
|
if target > current {
|
||||||
|
|
@ -292,15 +292,15 @@ impl Arranger {
|
||||||
fn cell_clip <'a> (
|
fn cell_clip <'a> (
|
||||||
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
||||||
) -> impl Content<TuiOut> + use<'a> {
|
) -> impl Content<TuiOut> + use<'a> {
|
||||||
scene.clips.get(index).map(|clip|clip.as_ref().map(|phrase|{
|
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
||||||
let phrase = phrase.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
let mut bg = TuiTheme::border_bg();
|
let mut bg = TuiTheme::border_bg();
|
||||||
let name = phrase.name.to_string();
|
let name = clip.name.to_string();
|
||||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||||
let color = phrase.color;
|
let color = clip.color;
|
||||||
bg = color.dark.rgb;
|
bg = color.dark.rgb;
|
||||||
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
||||||
if *playing.read().unwrap() == *phrase {
|
if *playing.read().unwrap() == *clip {
|
||||||
bg = color.light.rgb
|
bg = color.light.rgb
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -312,8 +312,8 @@ impl Arranger {
|
||||||
}
|
}
|
||||||
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
let w = self.size.w();
|
let w = self.size.w();
|
||||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
let pool_w = if self.pool.visible { phrase_w } else { 0 };
|
let pool_w = if self.pool.visible { clip_w } else { 0 };
|
||||||
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
||||||
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,21 +15,21 @@ pub enum GrooveboxCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
Self::Enqueue(phrase) => {
|
Self::Enqueue(clip) => {
|
||||||
state.player.enqueue_next(phrase.as_ref());
|
state.player.enqueue_next(clip.as_ref());
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
Self::Pool(cmd) => match cmd {
|
Self::Pool(cmd) => match cmd {
|
||||||
// autoselect: automatically load selected phrase in editor
|
// autoselect: automatically load selected clip in editor
|
||||||
PoolCommand::Select(_) => {
|
PoolCommand::Select(_) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
// update color in all places simultaneously
|
// update color in all places simultaneously
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
|
|
@ -52,10 +52,10 @@ handle!(TuiIn: |self: Groovebox, input|
|
||||||
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
||||||
// Tab: Toggle compact mode
|
// Tab: Toggle compact mode
|
||||||
key(Tab) => Cmd::Compact(!state.compact),
|
key(Tab) => Cmd::Compact(!state.compact),
|
||||||
// q: Enqueue currently edited phrase
|
// q: Enqueue currently edited clip
|
||||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
key(Char('q')) => Cmd::Enqueue(Some(state.pool.clip().clone())),
|
||||||
// 0: Enqueue phrase 0 (stop all)
|
// 0: Enqueue clip 0 (stop all)
|
||||||
key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
|
key(Char('0')) => Cmd::Enqueue(Some(state.pool.clips()[0].clone())),
|
||||||
// TODO: k: toggle on-screen keyboard
|
// TODO: k: toggle on-screen keyboard
|
||||||
ctrl(key(Char('k'))) => todo!("keyboard"),
|
ctrl(key(Char('k'))) => todo!("keyboard"),
|
||||||
// Transport: Play from start or rewind to start
|
// Transport: Play from start or rewind to start
|
||||||
|
|
@ -72,10 +72,10 @@ keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand
|
||||||
shift(key(Delete)) => Cmd::Sampler(
|
shift(key(Delete)) => Cmd::Sampler(
|
||||||
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
|
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
|
||||||
),
|
),
|
||||||
// e: Toggle between editing currently playing or other phrase
|
// e: Toggle between editing currently playing or other clip
|
||||||
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
||||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
||||||
let selected = state.pool.phrase().clone();
|
let selected = state.pool.clip().clone();
|
||||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||||
selected
|
selected
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
mod pool_tui; pub use self::pool_tui::*;
|
mod pool_tui; pub use self::pool_tui::*;
|
||||||
mod clip_length; pub use self::clip_length::*;
|
mod clip_length; pub use self::clip_length::*;
|
||||||
mod clip_rename; pub use self::clip_rename::*;
|
mod clip_rename; pub use self::clip_rename::*;
|
||||||
mod clip_select; pub use self::clip_select::*;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PoolModel {
|
pub struct PoolModel {
|
||||||
pub(crate) visible: bool,
|
pub(crate) visible: bool,
|
||||||
/// Collection of phrases
|
/// Collection of clips
|
||||||
pub(crate) phrases: Vec<Arc<RwLock<MidiClip>>>,
|
pub(crate) clips: Vec<Arc<RwLock<MidiClip>>>,
|
||||||
/// Selected phrase
|
/// Selected clip
|
||||||
pub(crate) phrase: AtomicUsize,
|
pub(crate) clip: AtomicUsize,
|
||||||
/// Mode switch
|
/// Mode switch
|
||||||
pub(crate) mode: Option<PoolMode>,
|
pub(crate) mode: Option<PoolMode>,
|
||||||
/// Rendered size
|
/// Rendered size
|
||||||
|
|
@ -20,29 +19,29 @@ pub struct PoolModel {
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modes for phrase pool
|
/// Modes for clip pool
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PoolMode {
|
pub enum PoolMode {
|
||||||
/// Renaming a pattern
|
/// Renaming a pattern
|
||||||
Rename(usize, Arc<str>),
|
Rename(usize, Arc<str>),
|
||||||
/// Editing the length of a pattern
|
/// Editing the length of a pattern
|
||||||
Length(usize, usize, PhraseLengthFocus),
|
Length(usize, usize, PhraseLengthFocus),
|
||||||
/// Load phrase from disk
|
/// Load clip from disk
|
||||||
Import(usize, FileBrowser),
|
Import(usize, FileBrowser),
|
||||||
/// Save phrase to disk
|
/// Save clip to disk
|
||||||
Export(usize, FileBrowser),
|
Export(usize, FileBrowser),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum PoolCommand {
|
pub enum PoolCommand {
|
||||||
Show(bool),
|
Show(bool),
|
||||||
/// Update the contents of the phrase pool
|
/// Update the contents of the clip pool
|
||||||
Phrase(MidiPoolCommand),
|
Phrase(MidiPoolCommand),
|
||||||
/// Select a phrase from the phrase pool
|
/// Select a clip from the clip pool
|
||||||
Select(usize),
|
Select(usize),
|
||||||
/// Rename a phrase
|
/// Rename a clip
|
||||||
Rename(PhraseRenameCommand),
|
Rename(PhraseRenameCommand),
|
||||||
/// Change the length of a phrase
|
/// Change the length of a clip
|
||||||
Length(PhraseLengthCommand),
|
Length(PhraseLengthCommand),
|
||||||
/// Import from file
|
/// Import from file
|
||||||
Import(FileBrowserCommand),
|
Import(FileBrowserCommand),
|
||||||
|
|
@ -59,9 +58,9 @@ command!(|self:PoolCommand, state: PoolModel|{
|
||||||
}
|
}
|
||||||
Rename(command) => match command {
|
Rename(command) => match command {
|
||||||
PhraseRenameCommand::Begin => {
|
PhraseRenameCommand::Begin => {
|
||||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
let length = state.clips()[state.clip_index()].read().unwrap().length;
|
||||||
*state.phrases_mode_mut() = Some(
|
*state.clips_mode_mut() = Some(
|
||||||
PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
PoolMode::Length(state.clip_index(), length, PhraseLengthFocus::Bar)
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
|
@ -69,9 +68,9 @@ command!(|self:PoolCommand, state: PoolModel|{
|
||||||
},
|
},
|
||||||
Length(command) => match command {
|
Length(command) => match command {
|
||||||
PhraseLengthCommand::Begin => {
|
PhraseLengthCommand::Begin => {
|
||||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
let name = state.clips()[state.clip_index()].read().unwrap().name.clone();
|
||||||
*state.phrases_mode_mut() = Some(
|
*state.clips_mode_mut() = Some(
|
||||||
PoolMode::Rename(state.phrase_index(), name)
|
PoolMode::Rename(state.clip_index(), name)
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
|
@ -79,8 +78,8 @@ command!(|self:PoolCommand, state: PoolModel|{
|
||||||
},
|
},
|
||||||
Import(command) => match command {
|
Import(command) => match command {
|
||||||
FileBrowserCommand::Begin => {
|
FileBrowserCommand::Begin => {
|
||||||
*state.phrases_mode_mut() = Some(
|
*state.clips_mode_mut() = Some(
|
||||||
PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?)
|
PoolMode::Import(state.clip_index(), FileBrowser::new(None)?)
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
|
@ -88,34 +87,34 @@ command!(|self:PoolCommand, state: PoolModel|{
|
||||||
},
|
},
|
||||||
Export(command) => match command {
|
Export(command) => match command {
|
||||||
FileBrowserCommand::Begin => {
|
FileBrowserCommand::Begin => {
|
||||||
*state.phrases_mode_mut() = Some(
|
*state.clips_mode_mut() = Some(
|
||||||
PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?)
|
PoolMode::Export(state.clip_index(), FileBrowser::new(None)?)
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
_ => command.execute(state)?.map(Export)
|
_ => command.execute(state)?.map(Export)
|
||||||
},
|
},
|
||||||
Select(phrase) => {
|
Select(clip) => {
|
||||||
state.set_phrase_index(phrase);
|
state.set_clip_index(clip);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.phrases_mode() {
|
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() {
|
||||||
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
|
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
|
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
||||||
_ => to_phrases_command(state, input)?
|
_ => to_clips_command(state, input)?
|
||||||
});
|
});
|
||||||
|
|
||||||
fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
fn to_clips_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||||
use KeyCode::{Up, Down, Delete, Char};
|
use KeyCode::{Up, Down, Delete, Char};
|
||||||
use PoolCommand as Cmd;
|
use PoolCommand as Cmd;
|
||||||
let index = state.phrase_index();
|
let index = state.clip_index();
|
||||||
let count = state.phrases().len();
|
let count = state.clips().len();
|
||||||
Some(match input {
|
Some(match input {
|
||||||
kpat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
kpat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
||||||
kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
||||||
|
|
@ -123,25 +122,25 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand>
|
||||||
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||||
kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
||||||
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||||
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
|
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||||
),
|
),
|
||||||
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||||
index.saturating_add(1) % state.phrases().len()
|
index.saturating_add(1) % state.clips().len()
|
||||||
),
|
),
|
||||||
kpat!(Char('<')) => if index > 1 {
|
kpat!(Char('<')) => if index > 1 {
|
||||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||||
Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index))
|
Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index))
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
},
|
},
|
||||||
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||||
state.set_phrase_index(state.phrase_index() + 1);
|
state.set_clip_index(state.clip_index() + 1);
|
||||||
Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index))
|
Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index))
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
},
|
},
|
||||||
kpat!(Delete) => if index > 0 {
|
kpat!(Delete) => if index > 0 {
|
||||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||||
Cmd::Phrase(MidiPoolCommand::Delete(index))
|
Cmd::Phrase(MidiPoolCommand::Delete(index))
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
|
|
@ -153,9 +152,9 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand>
|
||||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||||
))),
|
))),
|
||||||
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||||
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
|
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||||
Cmd::Phrase(MidiPoolCommand::Add(index + 1, phrase))
|
Cmd::Phrase(MidiPoolCommand::Add(index + 1, clip))
|
||||||
},
|
},
|
||||||
_ => return None
|
_ => return None
|
||||||
})
|
})
|
||||||
|
|
@ -164,33 +163,33 @@ impl Default for PoolModel {
|
||||||
fn default () -> Self {
|
fn default () -> Self {
|
||||||
Self {
|
Self {
|
||||||
visible: true,
|
visible: true,
|
||||||
phrases: vec![RwLock::new(MidiClip::default()).into()],
|
clips: vec![RwLock::new(MidiClip::default()).into()],
|
||||||
phrase: 0.into(),
|
clip: 0.into(),
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
mode: None,
|
mode: None,
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
from!(|phrase:&Arc<RwLock<MidiClip>>|PoolModel = {
|
from!(|clip:&Arc<RwLock<MidiClip>>|PoolModel = {
|
||||||
let mut model = Self::default();
|
let mut model = Self::default();
|
||||||
model.phrases.push(phrase.clone());
|
model.clips.push(clip.clone());
|
||||||
model.phrase.store(1, Relaxed);
|
model.clip.store(1, Relaxed);
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
has_phrases!(|self: PoolModel|self.phrases);
|
has_clips!(|self: PoolModel|self.clips);
|
||||||
has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]);
|
has_clip!(|self: PoolModel|self.clips[self.clip_index()]);
|
||||||
impl PoolModel {
|
impl PoolModel {
|
||||||
pub(crate) fn phrase_index (&self) -> usize {
|
pub(crate) fn clip_index (&self) -> usize {
|
||||||
self.phrase.load(Relaxed)
|
self.clip.load(Relaxed)
|
||||||
}
|
}
|
||||||
pub(crate) fn set_phrase_index (&self, value: usize) {
|
pub(crate) fn set_clip_index (&self, value: usize) {
|
||||||
self.phrase.store(value, Relaxed);
|
self.clip.store(value, Relaxed);
|
||||||
}
|
}
|
||||||
pub(crate) fn phrases_mode (&self) -> &Option<PoolMode> {
|
pub(crate) fn clips_mode (&self) -> &Option<PoolMode> {
|
||||||
&self.mode
|
&self.mode
|
||||||
}
|
}
|
||||||
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
pub(crate) fn clips_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||||
&mut self.mode
|
&mut self.mode
|
||||||
}
|
}
|
||||||
pub fn file_picker (&self) -> Option<&FileBrowser> {
|
pub fn file_picker (&self) -> Option<&FileBrowser> {
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ use PhraseLengthFocus::*;
|
||||||
use PhraseLengthCommand::*;
|
use PhraseLengthCommand::*;
|
||||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
||||||
|
|
||||||
/// Displays and edits phrase length.
|
/// Displays and edits clip length.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PhraseLength {
|
pub struct PhraseLength {
|
||||||
/// Pulses per beat (quaver)
|
/// Pulses per beat (quaver)
|
||||||
pub ppq: usize,
|
pub ppq: usize,
|
||||||
/// Beats per bar
|
/// Beats per bar
|
||||||
pub bpb: usize,
|
pub bpb: usize,
|
||||||
/// Length of phrase in pulses
|
/// Length of clip in pulses
|
||||||
pub pulses: usize,
|
pub pulses: usize,
|
||||||
/// Selected subdivision
|
/// Selected subdivision
|
||||||
pub focus: Option<PhraseLengthFocus>,
|
pub focus: Option<PhraseLengthFocus>,
|
||||||
|
|
@ -97,9 +97,9 @@ pub enum PhraseLengthCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
command!(|self:PhraseLengthCommand,state:PoolModel|{
|
command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||||
match state.phrases_mode_mut().clone() {
|
match state.clips_mode_mut().clone() {
|
||||||
Some(PoolMode::Length(phrase, ref mut length, ref mut focus)) => match self {
|
Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self {
|
||||||
Cancel => { *state.phrases_mode_mut() = None; },
|
Cancel => { *state.clips_mode_mut() = None; },
|
||||||
Prev => { focus.prev() },
|
Prev => { focus.prev() },
|
||||||
Next => { focus.next() },
|
Next => { focus.next() },
|
||||||
Inc => match focus {
|
Inc => match focus {
|
||||||
|
|
@ -113,11 +113,11 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||||
Tick => { *length = length.saturating_sub(1) },
|
Tick => { *length = length.saturating_sub(1) },
|
||||||
},
|
},
|
||||||
Set(length) => {
|
Set(length) => {
|
||||||
let mut phrase = state.phrases()[phrase].write().unwrap();
|
let mut clip = state.clips()[clip].write().unwrap();
|
||||||
let old_length = phrase.length;
|
let old_length = clip.length;
|
||||||
phrase.length = length;
|
clip.length = length;
|
||||||
std::mem::drop(phrase);
|
std::mem::drop(clip);
|
||||||
*state.phrases_mode_mut() = None;
|
*state.clips_mode_mut() = None;
|
||||||
return Ok(Some(Self::Set(old_length)))
|
return Ok(Some(Self::Set(old_length)))
|
||||||
},
|
},
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
|
|
@ -128,7 +128,7 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||||
});
|
});
|
||||||
|
|
||||||
input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{
|
input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{
|
||||||
if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() {
|
if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() {
|
||||||
match input {
|
match input {
|
||||||
kpat!(Up) => Self::Inc,
|
kpat!(Up) => Self::Inc,
|
||||||
kpat!(Down) => Self::Dec,
|
kpat!(Down) => Self::Dec,
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,19 @@ pub enum PhraseRenameCommand {
|
||||||
impl Command<PoolModel> for PhraseRenameCommand {
|
impl Command<PoolModel> for PhraseRenameCommand {
|
||||||
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
||||||
use PhraseRenameCommand::*;
|
use PhraseRenameCommand::*;
|
||||||
match state.phrases_mode_mut().clone() {
|
match state.clips_mode_mut().clone() {
|
||||||
Some(PoolMode::Rename(phrase, ref mut old_name)) => match self {
|
Some(PoolMode::Rename(clip, ref mut old_name)) => match self {
|
||||||
Set(s) => {
|
Set(s) => {
|
||||||
state.phrases()[phrase].write().unwrap().name = s;
|
state.clips()[clip].write().unwrap().name = s;
|
||||||
return Ok(Some(Self::Set(old_name.clone().into())))
|
return Ok(Some(Self::Set(old_name.clone().into())))
|
||||||
},
|
},
|
||||||
Confirm => {
|
Confirm => {
|
||||||
let old_name = old_name.clone();
|
let old_name = old_name.clone();
|
||||||
*state.phrases_mode_mut() = None;
|
*state.clips_mode_mut() = None;
|
||||||
return Ok(Some(Self::Set(old_name)))
|
return Ok(Some(Self::Set(old_name)))
|
||||||
},
|
},
|
||||||
Cancel => {
|
Cancel => {
|
||||||
state.phrases()[phrase].write().unwrap().name = old_name.clone().into();
|
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||||
},
|
},
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
},
|
},
|
||||||
|
|
@ -37,7 +37,7 @@ impl Command<PoolModel> for PhraseRenameCommand {
|
||||||
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
|
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
|
||||||
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
|
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
|
||||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() {
|
||||||
Some(match input {
|
Some(match input {
|
||||||
kpat!(Char(c)) => {
|
kpat!(Char(c)) => {
|
||||||
let mut new_name = old_name.clone().to_string();
|
let mut new_name = old_name.clone().to_string();
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
@ -3,14 +3,14 @@ use crate::*;
|
||||||
pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
|
pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
|
||||||
render!(TuiOut: (self: PoolView<'a>) => {
|
render!(TuiOut: (self: PoolView<'a>) => {
|
||||||
let Self(compact, model) = self;
|
let Self(compact, model) = self;
|
||||||
let PoolModel { phrases, mode, .. } = self.1;
|
let PoolModel { clips, mode, .. } = self.1;
|
||||||
let color = self.1.phrase().read().unwrap().color;
|
let color = self.1.clip().read().unwrap().color;
|
||||||
Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, Outer(
|
Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, Outer(
|
||||||
Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)
|
Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)
|
||||||
).enclose(Map::new(||model.phrases().iter(), |clip, i|{
|
).enclose(Map::new(||model.clips().iter(), |clip, i|{
|
||||||
let item_height = 1;
|
let item_height = 1;
|
||||||
let item_offset = i as u16 * item_height;
|
let item_offset = i as u16 * item_height;
|
||||||
let selected = i == model.phrase_index();
|
let selected = i == model.clip_index();
|
||||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ impl Sequencer {
|
||||||
}
|
}
|
||||||
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
let w = self.size.w();
|
let w = self.size.w();
|
||||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
let pool_w = if self.pool.visible { phrase_w } else { 0 };
|
let pool_w = if self.pool.visible { clip_w } else { 0 };
|
||||||
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
||||||
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ impl Sequencer {
|
||||||
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
||||||
double(("a", "append"), ("s", "set note"),),
|
double(("a", "append"), ("s", "set note"),),
|
||||||
double((",.", "length"), ("<>", "triplet"), ),
|
double((",.", "length"), ("<>", "triplet"), ),
|
||||||
double(("[]", "phrase"), ("{}", "order"), ),
|
double(("[]", "clip"), ("{}", "order"), ),
|
||||||
double(("q", "enqueue"), ("e", "edit"), ),
|
double(("q", "enqueue"), ("e", "edit"), ),
|
||||||
double(("c", "color"), ("", ""),),
|
double(("c", "color"), ("", ""),),
|
||||||
))
|
))
|
||||||
|
|
@ -95,7 +95,7 @@ audio!(|self:Sequencer, client, scope|{
|
||||||
});
|
});
|
||||||
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
||||||
has_clock!(|self:Sequencer|&self.player.clock);
|
has_clock!(|self:Sequencer|&self.player.clock);
|
||||||
has_phrases!(|self:Sequencer|self.pool.phrases);
|
has_clips!(|self:Sequencer|self.pool.clips);
|
||||||
has_editor!(|self:Sequencer|self.editor);
|
has_editor!(|self:Sequencer|self.editor);
|
||||||
handle!(TuiIn: |self:Sequencer,input|SequencerCommand::execute_with_state(self, input.event()));
|
handle!(TuiIn: |self:Sequencer,input|SequencerCommand::execute_with_state(self, input.event()));
|
||||||
#[derive(Clone, Debug)] pub enum SequencerCommand {
|
#[derive(Clone, Debug)] pub enum SequencerCommand {
|
||||||
|
|
@ -119,14 +119,14 @@ keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
||||||
key(Char('U')) => Cmd::History( 1),
|
key(Char('U')) => Cmd::History( 1),
|
||||||
// Tab: Toggle compact mode
|
// Tab: Toggle compact mode
|
||||||
key(Tab) => Cmd::Compact(!state.compact),
|
key(Tab) => Cmd::Compact(!state.compact),
|
||||||
// q: Enqueue currently edited phrase
|
// q: Enqueue currently edited clip
|
||||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
key(Char('q')) => Cmd::Enqueue(Some(state.pool.clip().clone())),
|
||||||
// 0: Enqueue phrase 0 (stop all)
|
// 0: Enqueue clip 0 (stop all)
|
||||||
key(Char('0')) => Cmd::Enqueue(Some(state.phrases()[0].clone())),
|
key(Char('0')) => Cmd::Enqueue(Some(state.clips()[0].clone())),
|
||||||
// e: Toggle between editing currently playing or other phrase
|
// e: Toggle between editing currently playing or other clip
|
||||||
key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
||||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
||||||
let selected = state.pool.phrase().clone();
|
let selected = state.pool.clip().clone();
|
||||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||||
selected
|
selected
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -143,21 +143,21 @@ keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
||||||
return None
|
return None
|
||||||
});
|
});
|
||||||
command!(|self: SequencerCommand, state: Sequencer|match self {
|
command!(|self: SequencerCommand, state: Sequencer|match self {
|
||||||
Self::Enqueue(phrase) => {
|
Self::Enqueue(clip) => {
|
||||||
state.player.enqueue_next(phrase.as_ref());
|
state.player.enqueue_next(clip.as_ref());
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
Self::Pool(cmd) => match cmd {
|
Self::Pool(cmd) => match cmd {
|
||||||
// autoselect: automatically load selected phrase in editor
|
// autoselect: automatically load selected clip in editor
|
||||||
PoolCommand::Select(_) => {
|
PoolCommand::Select(_) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
// update color in all places simultaneously
|
// update color in all places simultaneously
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
state.editor.set_clip(Some(state.pool.clip()));
|
||||||
undo
|
undo
|
||||||
},
|
},
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue