mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
243 lines
8.7 KiB
Rust
243 lines
8.7 KiB
Rust
//! MIDI editor.
|
|
use crate::*;
|
|
|
|
/// Contains state for viewing and editing a clip
|
|
pub struct MidiEditor {
|
|
pub mode: PianoHorizontal,
|
|
pub size: Measure<TuiOut>,
|
|
pub keys: SourceIter<'static>
|
|
}
|
|
|
|
impl std::fmt::Debug for MidiEditor {
|
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
f.debug_struct("MidiEditor")
|
|
.field("mode", &self.mode)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Default for MidiEditor {
|
|
fn default () -> Self {
|
|
Self {
|
|
mode: PianoHorizontal::new(None),
|
|
size: Measure::new(),
|
|
keys: SourceIter(include_str!("../../edn/keys_edit.edn")),
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
has_size!(<TuiOut>|self: MidiEditor|&self.size);
|
|
|
|
content!(TuiOut: |self: MidiEditor| {
|
|
self.autoscroll();
|
|
//self.autozoom();
|
|
self.size.of(&self.mode)
|
|
});
|
|
|
|
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
|
let model = Self::from(Some(clip.clone()));
|
|
model.redraw();
|
|
model
|
|
});
|
|
|
|
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
|
let mut model = Self::default();
|
|
*model.clip_mut() = clip;
|
|
model.redraw();
|
|
model
|
|
});
|
|
|
|
provide!(bool: |self: MidiEditor| {
|
|
":true" => true,
|
|
":false" => false,
|
|
":time-lock" => self.time_lock().get(),
|
|
":time-lock-toggle" => !self.time_lock().get(),
|
|
});
|
|
|
|
provide!(usize: |self: MidiEditor| {
|
|
":note-length" => self.note_len(),
|
|
|
|
":note-pos" => self.note_pos(),
|
|
":note-pos-next" => self.note_pos() + 1,
|
|
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
|
":note-pos-next-octave" => self.note_pos() + 12,
|
|
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
|
|
|
":note-len" => self.note_len(),
|
|
":note-len-next" => self.note_len() + 1,
|
|
":note-len-prev" => self.note_len().saturating_sub(1),
|
|
|
|
":note-range" => self.note_axis().get(),
|
|
":note-range-prev" => self.note_axis().get() + 1,
|
|
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
|
|
|
":time-pos" => self.time_pos(),
|
|
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
|
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
|
|
|
":time-zoom" => self.time_zoom().get(),
|
|
":time-zoom-next" => self.time_zoom().get() + 1,
|
|
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
|
});
|
|
|
|
impl MidiEditor {
|
|
|
|
/// Put note at current position
|
|
pub fn put_note (&mut self, advance: bool) {
|
|
let mut redraw = false;
|
|
if let Some(clip) = self.clip() {
|
|
let mut clip = clip.write().unwrap();
|
|
let note_start = self.time_pos();
|
|
let note_pos = self.note_pos();
|
|
let note_len = self.note_len();
|
|
let note_end = note_start + (note_len.saturating_sub(1));
|
|
let key: u7 = u7::from(note_pos as u8);
|
|
let vel: u7 = 100.into();
|
|
let length = clip.length;
|
|
let note_end = note_end % length;
|
|
let note_on = MidiMessage::NoteOn { key, vel };
|
|
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
|
|
clip.notes[note_start].push(note_on);
|
|
}
|
|
let note_off = MidiMessage::NoteOff { key, vel };
|
|
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
|
|
clip.notes[note_end].push(note_off);
|
|
}
|
|
if advance {
|
|
self.set_time_pos(note_end);
|
|
}
|
|
redraw = true;
|
|
}
|
|
if redraw {
|
|
self.mode.redraw();
|
|
}
|
|
}
|
|
|
|
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
|
|
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
|
(clip.color, clip.name.clone(), clip.length, clip.looped)
|
|
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
|
|
Bsp::e(
|
|
FieldH(color, "Edit", format!("{name} ({length})")),
|
|
FieldH(color, "Loop", looped.to_string())
|
|
)
|
|
}
|
|
|
|
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
|
|
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
|
(clip.color, clip.length)
|
|
} else { (ItemTheme::G[64], 0) };
|
|
let time_pos = self.time_pos();
|
|
let time_zoom = self.time_zoom().get();
|
|
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
|
|
let note_pos = format!("{:>3}", self.note_pos());
|
|
let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos()));
|
|
let note_len = format!("{:>4}", self.note_len());
|
|
Bsp::e(
|
|
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
|
|
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
|
|
)
|
|
}
|
|
|
|
//fn clip_length (&self) -> usize {
|
|
//self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
|
//}
|
|
}
|
|
|
|
impl TimeRange for MidiEditor {
|
|
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
|
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
|
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
|
|
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
|
|
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
|
}
|
|
|
|
impl NoteRange for MidiEditor {
|
|
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
|
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
|
}
|
|
|
|
impl NotePoint for MidiEditor {
|
|
fn note_len (&self) -> usize { self.mode.note_len() }
|
|
fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) }
|
|
fn note_pos (&self) -> usize { self.mode.note_pos() }
|
|
fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) }
|
|
}
|
|
|
|
impl TimePoint for MidiEditor {
|
|
fn time_pos (&self) -> usize { self.mode.time_pos() }
|
|
fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) }
|
|
}
|
|
|
|
impl MidiViewer for MidiEditor {
|
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
|
|
fn redraw (&self) { self.mode.redraw() }
|
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
|
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
|
|
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
|
}
|
|
|
|
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
|
("note/append" [] Some(Self::AppendNote))
|
|
("note/put" [] Some(Self::PutNote))
|
|
("note/del" [] Some(Self::DelNote))
|
|
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
|
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
|
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
|
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
|
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
|
|
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
|
|
});
|
|
|
|
#[derive(Clone, Debug)] pub enum MidiEditCommand {
|
|
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
|
AppendNote,
|
|
PutNote,
|
|
DelNote,
|
|
SetNoteCursor(usize),
|
|
SetNoteLength(usize),
|
|
SetNoteScroll(usize),
|
|
SetTimeCursor(usize),
|
|
SetTimeScroll(usize),
|
|
SetTimeZoom(usize),
|
|
SetTimeLock(bool),
|
|
Show(Option<Arc<RwLock<MidiClip>>>),
|
|
}
|
|
|
|
handle!(TuiIn: |self: MidiEditor, input|{
|
|
Ok(if let Some(command) = self.keys.command::<_, MidiEditCommand, _>(self, input) {
|
|
let _undo = command.execute(self)?;
|
|
Some(true)
|
|
} else {
|
|
None
|
|
})
|
|
});
|
|
|
|
impl Command<MidiEditor> for MidiEditCommand {
|
|
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
|
use MidiEditCommand::*;
|
|
match self {
|
|
Show(clip) => { state.set_clip(clip.as_ref()); },
|
|
DelNote => {},
|
|
PutNote => { state.put_note(false); },
|
|
AppendNote => { state.put_note(true); },
|
|
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
|
SetTimeLock(x) => { state.time_lock().set(x); },
|
|
SetTimeScroll(x) => { state.time_start().set(x); },
|
|
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
|
|
SetNoteLength(x) => {
|
|
let note_len = state.note_len();
|
|
let time_zoom = state.time_zoom().get();
|
|
state.set_note_len(x);
|
|
//if note_len / time_zoom != x / time_zoom {
|
|
state.redraw();
|
|
//}
|
|
},
|
|
SetTimeCursor(x) => { state.set_time_pos(x); },
|
|
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
|
|
//_ => todo!("{:?}", self)
|
|
}
|
|
Ok(None)
|
|
}
|
|
}
|