tek/src/midi_edit.rs
2026-09-23 16:08:54 +03:00

402 lines
14 KiB
Rust

#![allow(unused)]
use crate::*;
impl<T: HasEditor> EditorModel for T {}
pub trait EditorModel: HasEditor {
fn is_editing (&self) -> bool {
self.editor().is_some()
}
fn editor_w (&self) -> usize {
self.editor().as_ref().map(|e|e.size.w()).unwrap_or(0) as usize
}
fn editor_h (&self) -> usize {
self.editor().as_ref().map(|e|e.size.h()).unwrap_or(0) as usize
}
}
/// Contains state for viewing and editing a clip.
///
/// ```
/// use std::sync::Arc;
/// use parking_lot::RwLock;
/// use tek::{MidiClip, MidiEditor, PianoHorizontal};
/// let clip = MidiClip::stop_all();
/// let mode = PianoHorizontal::default();
/// let mut editor = MidiEditor { mode, size: Default::default() };
///
/// let _ = editor.put_note(true);
/// let _ = editor.put_note(false);
/// let _ = editor.clip_status();
/// let _ = editor.edit_status();
/// ```
pub struct MidiEditor {
/// View mode and state of editor
pub mode: PianoHorizontal,
/// Size of editor on screen
pub size: Sizer,
}
impl_default!(MidiEditor: Self {
mode: Default::default(),
size: Sizer(
Arc::new(0usize.into()),
Arc::new(0usize.into()),
),
});
impl MidiEditor {
/// Put note at current position
pub fn put_note (&mut self, advance: bool) {
let mut redraw = false;
if let Some(clip) = &*self.mode.clip.read() {
let note_start = self.get_time_pos();
let note_pos = self.get_note_pos();
let note_len = self.get_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 mut clip = clip.write();
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 + 1) % clip.length);
}
redraw = true;
}
if redraw {
self.mode.redraw();
}
}
fn clip_length (&self) -> usize {
self.mode.clip.map_inner(|p|p.length).unwrap_or(1)
}
fn note_length (&self) -> usize {
self.get_note_len()
}
fn note_pos (&self) -> usize {
self.get_note_pos()
}
fn note_pos_next (&self) -> usize {
self.get_note_pos() + 1
}
fn note_pos_next_octave (&self) -> usize {
self.get_note_pos() + 12
}
fn note_pos_prev (&self) -> usize {
self.get_note_pos().saturating_sub(1)
}
fn note_pos_prev_octave (&self) -> usize {
self.get_note_pos().saturating_sub(12)
}
fn note_len (&self) -> usize {
self.get_note_len()
}
fn note_len_next (&self) -> usize {
self.get_note_len() + 1
}
fn note_len_prev (&self) -> usize {
self.get_note_len().saturating_sub(1)
}
fn note_range (&self) -> usize {
self.get_note_axis()
}
fn note_range_next (&self) -> usize {
self.get_note_axis() + 1
}
fn note_range_prev (&self) -> usize {
self.get_note_axis().saturating_sub(1)
}
fn time_zoom (&self) -> usize { self.get_time_zoom() }
fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 }
fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 }
fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) }
fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) }
fn time_lock (&self) -> bool { self.get_time_lock() }
fn time_lock_toggled (&self) -> bool { !self.get_time_lock() }
fn time_pos (&self) -> usize { self.get_time_pos() }
fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() }
fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() }
fn time_pos_prev (&self) -> usize {
let step = self.get_note_len();
self.get_time_pos().overflowing_sub(step)
.0.min(self.clip_length().saturating_sub(step))
}
fn time_pos_prev_fine (&self) -> usize {
self.get_time_pos().overflowing_sub(1)
.0.min(self.clip_length().saturating_sub(1))
}
}
impl <T: NotePoint+TimePoint> MidiPoint for T {}
impl <T: TimeRange+NoteRange> MidiRange for T {}
pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync {
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
fn redraw (&self);
/// Make sure cursor is within note range
fn autoscroll (&self) {
let note_pos = self.get_note_pos().min(127);
let note_lo = self.get_note_lo();
let note_hi = self.get_note_hi();
if note_pos < note_lo {
self.note_lo().store(note_pos, Relaxed);
} else if note_pos > note_hi {
self.note_lo().store((note_lo + note_pos).saturating_sub(note_hi), Relaxed);
}
}
/// Make sure time range is within display
fn autozoom (&self) {
if self.time_lock().load(Relaxed) {
let time_len = self.get_time_len();
let time_axis = self.get_time_axis();
let time_zoom = self.get_time_zoom();
loop {
let time_zoom = self.time_zoom().load(Relaxed);
let time_area = time_axis * time_zoom;
if time_area > time_len {
let next_time_zoom = note_duration_prev(time_zoom);
if next_time_zoom <= 1 {
break
}
let next_time_area = time_axis * next_time_zoom;
if next_time_area >= time_len {
self.time_zoom().store(next_time_zoom, Relaxed);
} else {
break
}
} else if time_area < time_len {
let prev_time_zoom = note_duration_next(time_zoom);
if prev_time_zoom > 384 {
break
}
let prev_time_area = time_axis * prev_time_zoom;
if prev_time_area <= time_len {
self.time_zoom().store(prev_time_zoom, Relaxed);
} else {
break
}
}
}
if time_zoom != self.time_zoom().load(Relaxed) {
self.redraw()
}
}
//while time_len.div_ceil(time_zoom) > time_axis {
//println!("\r{time_len} {time_zoom} {time_axis}");
//time_zoom = Note::next(time_zoom);
//}
//self.time_zoom().set(time_zoom);
}
}
impl_has!(Sizer: |self: MidiEditor| self.size);
impl std::fmt::Debug for MidiEditor {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiEditor").field("mode", &self.mode).finish()
}
}
impl NotePoint for MidiEditor {
fn note_len (&self) -> &AtomicUsize { self.mode.note_len() }
fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() }
}
impl TimePoint for MidiEditor {
fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() }
}
impl MidiViewer for MidiEditor {
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
self.mode.buffer_size(clip)
}
fn redraw (&self) {
self.mode.redraw()
}
}
/// 12 piano keys, some highlighted.
///
/// ```
/// let keys = tek::OctaveVertical::default();
/// ```
#[derive(Copy, Clone)]
pub struct OctaveVertical {
pub on: [bool; 12],
pub colors: [Color; 3]
}
impl OctaveVertical {
pub fn color (&self, pitch: usize) -> Color {
let pitch = pitch % 12;
self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }]
}
}
impl_default!(OctaveVertical: Self {
on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
});
/// A clip, rendered as a horizontal piano roll.
///
/// Uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
///
/// ```
/// let piano = tek::PianoHorizontal::default();
/// ```
#[derive(Clone, Default)] pub struct PianoHorizontal {
pub clip: ClipSlot,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Sizer,
/// The display window
pub range: MidiSelection,
/// The note cursor
pub point: MidiCursor,
/// The highlight color palette
pub color: ItemTheme,
/// Width of the keyboard
pub keys_width: u16,
}
impl_has!(Sizer: |self: PianoHorizontal| self.size);
impl PianoHorizontal {
pub fn new () -> Self {
let size = Sizer::default();
let mut range = MidiSelection::from((12, true));
range.time_axis = size.0.clone();
range.note_axis = size.1.clone();
let piano = Self { keys_width: 5, size, range, ..Default::default() };
piano.redraw();
piano
}
}
impl TimeRange for PianoHorizontal {
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
}
impl NoteRange for PianoHorizontal {
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
}
impl NotePoint for PianoHorizontal {
fn note_len (&self) -> &AtomicUsize { self.point.note_len() }
fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() }
}
impl TimePoint for PianoHorizontal {
fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() }
}
impl MidiViewer for PianoHorizontal {
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
let zoom = self.range.time_zoom().load(Relaxed);
let size = if zoom > 0 { clip.length / zoom } else { 0 };
(size, 128)
}
/// Draw the piano roll into the buffer.
fn redraw (&self) {
*self.buffer.write() = self.clip.map_inner(|clip|{
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let time_zoom = self.get_time_zoom().max(1);
self.time_len().store(clip.length, Relaxed);
let note_len = self.get_note_len();
let note_pos = self.get_note_pos();
let time_pos = self.get_time_pos();
draw_piano_fg(&mut buffer, &clip, time_zoom);
buffer
}).unwrap_or_default()
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let buffer = self.buffer.read();
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
.finish()
}
}
//impl App {
///// Toggle MIDI editor.
/////
///// ```
///// tek::App::default().toggle_editor(None);
///// ```
//pub fn toggle_editor (&mut self, value: Option<bool>) {
////FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
//if value.unwrap_or_else(||!self.editor().is_some()) {
//self.toggle_editor_on()
//} else {
//self.toggle_editor_off()
//}
//}
//fn toggle_editor_on (&mut self) {
//// Create new clip in pool when entering empty cell
//if let Selection::TrackClip { track: track_index, scene } = *self.selection()
//&& let Some(scene) = self.project.scenes.get_mut(scene)
//&& scene.clips.get(track_index).is_none()
//&& let Some(track) = self.project.tracks.get_mut(track_index)
//{
//let color = track.color.base.mix(scene.color.base, 0.5);
//let color = ItemColor::random_near(color, 0.2).into();
//let clip: MidiClip = MidiClip { color, ..Default::default() };
//let clip = self.project.clips.append(Some(clip));
//// autocolor: new clip colors from scene and track color
//if let Some(editor) = &mut self.project.editor {
//*editor.mode.clip.write() = clip.0.read().clone();
//editor.mode.color = clip.0.read().as_ref().map(|p|p.read().color)
//.unwrap_or(ItemTheme::G[64]);
//}
//*scene.clips.slot(track_index).write() = clip.0.read().clone();
////Some(clip)
//} else {
////None
//}
//}
//fn toggle_editor_off (&mut self) {
//if let Selection::TrackClip { track, scene } = *self.selection()
//&& let Some(scene) = self.project.scenes.get_mut(scene)
//{
//// Remove clip from arrangement when exiting empty clip editor
//let mut swapped = None;
//if let Some(count) = scene.clips.slot(track).read().as_ref().map(|c|c.read().count_midi_messages())
//&& count == 0
//{
//std::mem::swap(&mut swapped, &mut scene.clips.slot(track).write());
//}
//if let Some(clip) = swapped {
//self.project.clips.remove(&*clip.read());
//}
//}
//}
//}