mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-07-17 07:46:57 +02:00
restruct: 30e
This commit is contained in:
parent
8b6ab2fd08
commit
c737c7d839
13 changed files with 771 additions and 747 deletions
11
src/app.rs
11
src/app.rs
|
|
@ -87,11 +87,11 @@ impl App {
|
||||||
|
|
||||||
pub fn new_shared_run (
|
pub fn new_shared_run (
|
||||||
exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
exit: &Exit, jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||||||
) -> Usually<(Task, Task)> {
|
) -> Usually<(Self, Task, Task)> {
|
||||||
let state = Self::new_shared(&jack, project, config, mode);
|
let state = Self::new_shared(&jack, project, config, mode);
|
||||||
let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?;
|
let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?;
|
||||||
let terminal = tui_output(&exit, &state, Duration::from_millis(10))?;
|
let terminal = tui_output(&exit, &state, Duration::from_millis(10))?;
|
||||||
Ok((keyboard, terminal))
|
Ok((state, keyboard, terminal))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
|
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
|
||||||
|
|
@ -361,10 +361,10 @@ impl HasJack<'static> for App {
|
||||||
|
|
||||||
def_command!(AppCommand: |app: App| {
|
def_command!(AppCommand: |app: App| {
|
||||||
Nop => Ok(None),
|
Nop => Ok(None),
|
||||||
Confirm => tek_confirm(app),
|
|
||||||
Cancel => todo!(), // TODO delegate:
|
Cancel => todo!(), // TODO delegate:
|
||||||
Inc { axis: ControlAxis } => tek_inc(app, axis),
|
Confirm => app.confirm(),
|
||||||
Dec { axis: ControlAxis } => tek_dec(app, axis),
|
Inc { axis: ControlAxis } => app.inc(axis),
|
||||||
|
Dec { axis: ControlAxis } => app.dec(axis),
|
||||||
SetDialog { dialog: Dialog } => {
|
SetDialog { dialog: Dialog } => {
|
||||||
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||||||
},
|
},
|
||||||
|
|
@ -536,6 +536,7 @@ fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usua
|
||||||
h_exact(2, origin_n(w_full(item)))
|
h_exact(2, origin_n(w_full(item)))
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
Ok(to.area().into())
|
||||||
})))
|
})))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ impl Action {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tek_project_new (
|
pub fn tek_project_new (
|
||||||
jack: &Jack,
|
jack: &Jack<'static>,
|
||||||
clock: Clock,
|
clock: Clock,
|
||||||
left_from: &[impl AsRef<str>],
|
left_from: &[impl AsRef<str>],
|
||||||
left_to: &[impl AsRef<str>],
|
left_to: &[impl AsRef<str>],
|
||||||
|
|
@ -154,9 +154,7 @@ pub fn tek_project_new (
|
||||||
let right_tos = Connect::collect(right_to, empty, empty);
|
let right_tos = Connect::collect(right_to, empty, empty);
|
||||||
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||||
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
|
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
|
||||||
|
Ok(Arrangement::new(
|
||||||
// Create initial project:
|
|
||||||
let mut project = Arrangement::new(
|
|
||||||
jack,
|
jack,
|
||||||
None,
|
None,
|
||||||
clock,
|
clock,
|
||||||
|
|
@ -168,8 +166,7 @@ pub fn tek_project_new (
|
||||||
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
|
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
|
||||||
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
|
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
);
|
))
|
||||||
Ok(project)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tek_show_version () {
|
pub fn tek_show_version () {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl Config {
|
||||||
const CONFIG: &'static str = "tek.edn";
|
const CONFIG: &'static str = "tek.edn";
|
||||||
const DEFAULTS: &'static str = include_str!("../tek.edn");
|
const DEFAULTS: &'static str = include_str!("../tek.edn");
|
||||||
|
|
||||||
pub fn watch <T> (callback: impl Fn(Self)->T) -> Usually<T> {
|
pub fn watch <T> (callback: impl FnOnce(Self)->T) -> Usually<T> {
|
||||||
let config = Self::init_new(None)?;
|
let config = Self::init_new(None)?;
|
||||||
let result = callback(config);
|
let result = callback(config);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ pub mod clock; pub use self::clock::*;
|
||||||
pub mod dialog; pub use self::dialog::*;
|
pub mod dialog; pub use self::dialog::*;
|
||||||
pub mod editor; pub use self::editor::*;
|
pub mod editor; pub use self::editor::*;
|
||||||
pub mod menu; pub use self::menu::*;
|
pub mod menu; pub use self::menu::*;
|
||||||
|
pub mod meter; pub use self::meter::*;
|
||||||
pub mod mix; pub use self::mix::*;
|
pub mod mix; pub use self::mix::*;
|
||||||
pub mod port; pub use self::port::*;
|
pub mod port; pub use self::port::*;
|
||||||
pub mod sample; pub use self::sample::*;
|
pub mod sample; pub use self::sample::*;
|
||||||
|
|
|
||||||
|
|
@ -387,16 +387,26 @@ impl ClockView {
|
||||||
let lat = chunk / rate * 1000.;
|
let lat = chunk / rate * 1000.;
|
||||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||||
let mut cache = cache.write().unwrap();
|
let mut cache = cache.write().unwrap();
|
||||||
cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}"));
|
|
||||||
cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms"));
|
cache.buf.update(
|
||||||
cache.sr.update(Some((compact, rate)), |buf,_,_|{
|
Some(chunk), rewrite!(buf, "{chunk}")
|
||||||
buf.clear();
|
);
|
||||||
if compact {
|
|
||||||
write!(buf, "{:.1}kHz", rate / 1000.)
|
cache.lat.update(
|
||||||
} else {
|
Some(lat), rewrite!(buf, "{lat:.1}ms")
|
||||||
write!(buf, "{:.0}Hz", rate)
|
);
|
||||||
|
|
||||||
|
cache.sr.update(
|
||||||
|
Some((compact, rate)), |buf: &mut String,_,_|{
|
||||||
|
buf.clear();
|
||||||
|
if compact {
|
||||||
|
write!(buf, "{:.1}kHz", rate / 1000.)
|
||||||
|
} else {
|
||||||
|
write!(buf, "{:.0}Hz", rate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
|
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
|
||||||
let pulse = clock.timebase.usecs_to_pulse(now);
|
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||||
let time = now/1000000.;
|
let time = now/1000000.;
|
||||||
|
|
|
||||||
|
|
@ -22,82 +22,116 @@ pub struct MidiEditor {
|
||||||
pub mode: PianoHorizontal,
|
pub mode: PianoHorizontal,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A clip, rendered as a horizontal piano roll.
|
impl_default!(MidiEditor: Self {
|
||||||
///
|
size: [0, 0].into(), mode: PianoHorizontal::new(None)
|
||||||
/// ```
|
});
|
||||||
/// let piano = tek::PianoHorizontal::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
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: Size,
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 12 piano keys, some highlighted.
|
impl MidiEditor {
|
||||||
///
|
/// Put note at current position
|
||||||
/// ```
|
pub fn put_note (&mut self, advance: bool) {
|
||||||
/// let keys = tek::OctaveVertical::default();
|
let mut redraw = false;
|
||||||
/// ```
|
if let Some(clip) = self.clip() {
|
||||||
#[derive(Copy, Clone)]
|
let mut clip = clip.write().unwrap();
|
||||||
pub struct OctaveVertical {
|
let note_start = self.get_time_pos();
|
||||||
pub on: [bool; 12],
|
let note_pos = self.get_note_pos();
|
||||||
pub colors: [Color; 3]
|
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 length = clip.length;
|
||||||
/// let _ = tek::MidiCursor::default();
|
let note_end = note_end % length;
|
||||||
/// ```
|
let note_on = MidiMessage::NoteOn { key, vel };
|
||||||
#[derive(Debug, Clone)] pub struct MidiCursor {
|
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
|
||||||
/// Time coordinate of cursor
|
clip.notes[note_start].push(note_on);
|
||||||
pub time_pos: Arc<AtomicUsize>,
|
}
|
||||||
/// Note coordinate of cursor
|
let note_off = MidiMessage::NoteOff { key, vel };
|
||||||
pub note_pos: Arc<AtomicUsize>,
|
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
|
||||||
/// Length of note that will be inserted, in pulses
|
clip.notes[note_end].push(note_off);
|
||||||
pub note_len: Arc<AtomicUsize>,
|
}
|
||||||
}
|
if advance {
|
||||||
|
self.set_time_pos((note_end + 1) % clip.length);
|
||||||
///
|
}
|
||||||
/// ```
|
redraw = true;
|
||||||
/// use tek::{TimeRange, NoteRange};
|
}
|
||||||
/// let model = tek::MidiSelection::from((1, false));
|
if redraw {
|
||||||
///
|
self.mode.redraw();
|
||||||
/// let _ = model.get_time_len();
|
}
|
||||||
/// let _ = model.get_time_zoom();
|
}
|
||||||
/// let _ = model.get_time_lock();
|
fn _todo_opt_clip_stub (&self) -> Option<Arc<RwLock<MidiClip>>> { todo!() }
|
||||||
/// let _ = model.get_time_start();
|
fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) }
|
||||||
/// let _ = model.get_time_axis();
|
fn note_length (&self) -> usize { self.get_note_len() }
|
||||||
/// let _ = model.get_time_end();
|
fn note_pos (&self) -> usize { self.get_note_pos() }
|
||||||
///
|
fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 }
|
||||||
/// let _ = model.get_note_lo();
|
fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 }
|
||||||
/// let _ = model.get_note_axis();
|
fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) }
|
||||||
/// let _ = model.get_note_hi();
|
fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) }
|
||||||
/// ```
|
fn note_len (&self) -> usize { self.get_note_len() }
|
||||||
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
|
fn note_len_next (&self) -> usize { self.get_note_len() + 1 }
|
||||||
pub time_len: Arc<AtomicUsize>,
|
fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) }
|
||||||
/// Length of visible time axis
|
fn note_range (&self) -> usize { self.get_note_axis() }
|
||||||
pub time_axis: Arc<AtomicUsize>,
|
fn note_range_next (&self) -> usize { self.get_note_axis() + 1 }
|
||||||
/// Earliest time displayed
|
fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) }
|
||||||
pub time_start: Arc<AtomicUsize>,
|
fn time_zoom (&self) -> usize { self.get_time_zoom() }
|
||||||
/// Time step
|
fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 }
|
||||||
pub time_zoom: Arc<AtomicUsize>,
|
fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 }
|
||||||
/// Auto rezoom to fit in time axis
|
fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) }
|
||||||
pub time_lock: Arc<AtomicBool>,
|
fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) }
|
||||||
/// Length of visible note axis
|
fn time_lock (&self) -> bool { self.get_time_lock() }
|
||||||
pub note_axis: Arc<AtomicUsize>,
|
fn time_lock_toggled (&self) -> bool { !self.get_time_lock() }
|
||||||
// Lowest note displayed
|
fn time_pos (&self) -> usize { self.get_time_pos() }
|
||||||
pub note_lo: Arc<AtomicUsize>,
|
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))
|
||||||
|
}
|
||||||
|
pub fn clip_status (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
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) };
|
||||||
|
w_exact(20, south!(
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("f2", "name ", false),
|
||||||
|
w_full(origin_e(fg(Rgb(255, 255, 255), format!("{name} "))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("l", "ength ", false),
|
||||||
|
w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("r", "epeat ", false),
|
||||||
|
w_full(origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))))))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn edit_status (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
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.get_time_pos();
|
||||||
|
let time_zoom = self.get_time_zoom();
|
||||||
|
let time_lock = if self.get_time_lock() { "[lock]" } else { " " };
|
||||||
|
let note_pos = self.get_note_pos();
|
||||||
|
let note_name = format!("{:4}", note_pitch_to_name(note_pos));
|
||||||
|
let note_pos = format!("{:>3}", note_pos);
|
||||||
|
let note_len = format!("{:>4}", self.get_note_len());
|
||||||
|
w_exact(20, south!(
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("t", "ime ", false),
|
||||||
|
w_full(origin_e(fg(Rgb(255, 255, 255),
|
||||||
|
format!("{length} /{time_zoom} +{time_pos} "))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("z", "lock ", false),
|
||||||
|
w_full(origin_e(fg(Rgb(255, 255, 255),
|
||||||
|
format!("{time_lock}"))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("x", "note ", false),
|
||||||
|
w_full(origin_e(fg(Rgb(255, 255, 255),
|
||||||
|
format!("{note_name} {note_pos} {note_len}"))))))),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -217,257 +251,32 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait NotePoint {
|
|
||||||
fn note_len (&self) -> &AtomicUsize;
|
|
||||||
/// Get the current length of the note cursor.
|
|
||||||
fn get_note_len (&self) -> usize {
|
|
||||||
self.note_len().load(Relaxed)
|
|
||||||
}
|
|
||||||
/// Set the length of the note cursor, returning the previous value.
|
|
||||||
fn set_note_len (&self, x: usize) -> usize {
|
|
||||||
self.note_len().swap(x, Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn note_pos (&self) -> &AtomicUsize;
|
|
||||||
/// Get the current pitch of the note cursor.
|
|
||||||
fn get_note_pos (&self) -> usize {
|
|
||||||
self.note_pos().load(Relaxed).min(127)
|
|
||||||
}
|
|
||||||
/// Set the current pitch fo the note cursor, returning the previous value.
|
|
||||||
fn set_note_pos (&self, x: usize) -> usize {
|
|
||||||
self.note_pos().swap(x.min(127), Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TimePoint {
|
|
||||||
fn time_pos (&self) -> &AtomicUsize;
|
|
||||||
/// Get the current time position of the note cursor.
|
|
||||||
fn get_time_pos (&self) -> usize {
|
|
||||||
self.time_pos().load(Relaxed)
|
|
||||||
}
|
|
||||||
/// Set the current time position of the note cursor, returning the previous value.
|
|
||||||
fn set_time_pos (&self, x: usize) -> usize {
|
|
||||||
self.time_pos().swap(x, Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiPoint: NotePoint + TimePoint {
|
|
||||||
/// Get the current end of the note cursor.
|
|
||||||
fn get_note_end (&self) -> usize {
|
|
||||||
self.get_time_pos() + self.get_note_len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TimeRange {
|
|
||||||
fn time_len (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_len (&self) -> usize {
|
|
||||||
self.time_len().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn time_zoom (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_zoom (&self) -> usize {
|
|
||||||
self.time_zoom().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn set_time_zoom (&self, value: usize) -> usize {
|
|
||||||
self.time_zoom().swap(value, Relaxed)
|
|
||||||
}
|
|
||||||
fn time_lock (&self) -> &AtomicBool;
|
|
||||||
fn get_time_lock (&self) -> bool {
|
|
||||||
self.time_lock().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn set_time_lock (&self, value: bool) -> bool {
|
|
||||||
self.time_lock().swap(value, Relaxed)
|
|
||||||
}
|
|
||||||
fn time_start (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_start (&self) -> usize {
|
|
||||||
self.time_start().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn set_time_start (&self, value: usize) -> usize {
|
|
||||||
self.time_start().swap(value, Relaxed)
|
|
||||||
}
|
|
||||||
fn time_axis (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_axis (&self) -> usize {
|
|
||||||
self.time_axis().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn get_time_end (&self) -> usize {
|
|
||||||
self.time_start().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait NoteRange {
|
|
||||||
fn note_lo (&self) -> &AtomicUsize;
|
|
||||||
fn note_axis (&self) -> &AtomicUsize;
|
|
||||||
|
|
||||||
fn get_note_lo (&self) -> usize {
|
|
||||||
self.note_lo().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn set_note_lo (&self, x: usize) -> usize {
|
|
||||||
self.note_lo().swap(x, Relaxed)
|
|
||||||
}
|
|
||||||
fn get_note_axis (&self) -> usize {
|
|
||||||
self.note_axis().load(Relaxed)
|
|
||||||
}
|
|
||||||
fn get_note_hi (&self) -> usize {
|
|
||||||
(self.get_note_lo() + self.get_note_axis().saturating_sub(1)).min(127)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiRange: TimeRange + NoteRange {}
|
|
||||||
|
|
||||||
impl_has!(Size: |self: MidiEditor| self.size);
|
impl_has!(Size: |self: MidiEditor| self.size);
|
||||||
impl_has!(Size: |self: PianoHorizontal| self.size);
|
|
||||||
|
|
||||||
impl Draw<Tui> for MidiEditor {
|
impl Draw<Tui> for MidiEditor {
|
||||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
self.mode.tui().draw(to)
|
self.mode.tui().draw(to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Draw<Tui> for PianoHorizontal {
|
|
||||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
|
||||||
self.tui().draw(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Debug for MidiEditor {
|
impl std::fmt::Debug for MidiEditor {
|
||||||
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
f.debug_struct("MidiEditor").field("mode", &self.mode).finish()
|
f.debug_struct("MidiEditor").field("mode", &self.mode).finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(MidiEditor: |clip: &Arc<RwLock<MidiClip>>| {
|
impl_from!(MidiEditor: |clip: &Arc<RwLock<MidiClip>>| {
|
||||||
let model = Self::from(Some(clip.clone()));
|
let model = Self::from(Some(clip.clone()));
|
||||||
model.redraw();
|
model.redraw();
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
|
|
||||||
impl_from!(MidiEditor: |clip: Option<Arc<RwLock<MidiClip>>>| {
|
impl_from!(MidiEditor: |clip: Option<Arc<RwLock<MidiClip>>>| {
|
||||||
let mut model = Self::default();
|
let mut model = Self::default();
|
||||||
*model.clip_mut() = clip;
|
*model.clip_mut() = clip;
|
||||||
model.redraw();
|
model.redraw();
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
impl_default!(MidiEditor: Self {
|
|
||||||
size: [0, 0].into(), mode: PianoHorizontal::new(None)
|
|
||||||
});
|
|
||||||
impl_default!(OctaveVertical: Self {
|
|
||||||
on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
|
||||||
});
|
|
||||||
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.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 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 _todo_opt_clip_stub (&self) -> Option<Arc<RwLock<MidiClip>>> { todo!() }
|
|
||||||
fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().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))
|
|
||||||
}
|
|
||||||
pub fn clip_status (&self) -> impl Draw<Tui> + '_ {
|
|
||||||
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) };
|
|
||||||
w_exact(20, south!(
|
|
||||||
w_full(origin_w(east(
|
|
||||||
button_2("f2", "name ", false),
|
|
||||||
w_full(origin_e(fg(Rgb(255, 255, 255), format!("{name} "))))))),
|
|
||||||
w_full(origin_w(east(
|
|
||||||
button_2("l", "ength ", false),
|
|
||||||
w_full(origin_e(fg(Rgb(255, 255, 255), format!("{length} "))))))),
|
|
||||||
w_full(origin_w(east(
|
|
||||||
button_2("r", "epeat ", false),
|
|
||||||
w_full(origin_e(fg(Rgb(255, 255, 255), format!("{looped} "))))))),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
pub fn edit_status (&self) -> impl Draw<Tui> + '_ {
|
|
||||||
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.get_time_pos();
|
|
||||||
let time_zoom = self.get_time_zoom();
|
|
||||||
let time_lock = if self.get_time_lock() { "[lock]" } else { " " };
|
|
||||||
let note_pos = self.get_note_pos();
|
|
||||||
let note_name = format!("{:4}", note_pitch_to_name(note_pos));
|
|
||||||
let note_pos = format!("{:>3}", note_pos);
|
|
||||||
let note_len = format!("{:>4}", self.get_note_len());
|
|
||||||
w_exact(20, south!(
|
|
||||||
w_full(origin_w(east(
|
|
||||||
button_2("t", "ime ", false),
|
|
||||||
w_full(origin_e(fg(Rgb(255, 255, 255),
|
|
||||||
format!("{length} /{time_zoom} +{time_pos} "))))))),
|
|
||||||
w_full(origin_w(east(
|
|
||||||
button_2("z", "lock ", false),
|
|
||||||
w_full(origin_e(fg(Rgb(255, 255, 255),
|
|
||||||
format!("{time_lock}"))))))),
|
|
||||||
w_full(origin_w(east(
|
|
||||||
button_2("x", "note ", false),
|
|
||||||
w_full(origin_e(fg(Rgb(255, 255, 255),
|
|
||||||
format!("{note_name} {note_pos} {note_len}"))))))),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
impl NotePoint for MidiEditor {
|
||||||
fn note_len (&self) -> &AtomicUsize { self.mode.note_len() }
|
fn note_len (&self) -> &AtomicUsize { self.mode.note_len() }
|
||||||
|
|
@ -494,339 +303,7 @@ impl View<Tui> for MidiEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod octave; pub use self::octave::*;
|
||||||
impl PianoHorizontal {
|
mod piano; pub use self::piano::*;
|
||||||
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
mod point; pub use self::point::*;
|
||||||
let size = [0, 0].into();
|
mod range; pub use self::range::*;
|
||||||
let mut range = MidiSelection::from((12, true));
|
|
||||||
range.time_axis = size.x.clone();
|
|
||||||
range.note_axis = size.y.clone();
|
|
||||||
let piano = Self {
|
|
||||||
keys_width: 5,
|
|
||||||
size,
|
|
||||||
range,
|
|
||||||
buffer: RwLock::new(Default::default()).into(),
|
|
||||||
point: MidiCursor::default(),
|
|
||||||
clip: clip.cloned(),
|
|
||||||
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
|
|
||||||
};
|
|
||||||
piano.redraw();
|
|
||||||
piano
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PianoHorizontal {
|
|
||||||
fn tui (&self) -> impl Draw<Tui> {
|
|
||||||
south(
|
|
||||||
east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()),
|
|
||||||
east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PianoHorizontal {
|
|
||||||
|
|
||||||
/// Draw the piano roll background.
|
|
||||||
///
|
|
||||||
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
|
||||||
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) {
|
|
||||||
for (y, note) in (0..=127).rev().enumerate() {
|
|
||||||
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
|
||||||
let cell = buf.get_mut(x, y).unwrap();
|
|
||||||
if note == (127-note_point) || time == time_point {
|
|
||||||
cell.set_bg(Rgb(0,0,0));
|
|
||||||
} else {
|
|
||||||
cell.set_bg(clip.color.darkest.term);
|
|
||||||
}
|
|
||||||
if time % 384 == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.term);
|
|
||||||
cell.set_char('│');
|
|
||||||
} else if time % 96 == 0 {
|
|
||||||
cell.set_fg(clip.color.dark.term);
|
|
||||||
cell.set_char('╎');
|
|
||||||
} else if time % note_len == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.term);
|
|
||||||
cell.set_char('┊');
|
|
||||||
} else if (127 - note) % 12 == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.term);
|
|
||||||
cell.set_char('=');
|
|
||||||
} else if (127 - note) % 6 == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.term);
|
|
||||||
cell.set_char('—');
|
|
||||||
} else {
|
|
||||||
cell.set_fg(clip.color.darker.term);
|
|
||||||
cell.set_char('·');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw the piano roll foreground.
|
|
||||||
///
|
|
||||||
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
|
||||||
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
|
|
||||||
let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0));
|
|
||||||
let mut notes_on = [false;128];
|
|
||||||
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
|
|
||||||
for (_y, note) in (0..=127).rev().enumerate() {
|
|
||||||
if let Some(cell) = buf.get_mut(x, note) {
|
|
||||||
if notes_on[note] {
|
|
||||||
cell.set_char('▂');
|
|
||||||
cell.set_style(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let time_end = time_start + zoom;
|
|
||||||
for time in time_start..time_end.min(clip.length) {
|
|
||||||
for event in clip.notes[time].iter() {
|
|
||||||
match event {
|
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
|
||||||
let note = key.as_int() as usize;
|
|
||||||
if let Some(cell) = buf.get_mut(x, note) {
|
|
||||||
cell.set_char('█');
|
|
||||||
cell.set_style(style);
|
|
||||||
}
|
|
||||||
notes_on[note] = true
|
|
||||||
},
|
|
||||||
MidiMessage::NoteOff { key, .. } => {
|
|
||||||
notes_on[key.as_int() as usize] = false
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notes (&self) -> impl Draw<Tui> {
|
|
||||||
let time_start = self.get_time_start();
|
|
||||||
let note_lo = self.get_note_lo();
|
|
||||||
let note_hi = self.get_note_hi();
|
|
||||||
let buffer = self.buffer.clone();
|
|
||||||
thunk(move|to: &mut Tui|{
|
|
||||||
let xywh = to.area().into();
|
|
||||||
let XYWH(x0, y0, w, _h) = xywh;
|
|
||||||
let source = buffer.read().unwrap();
|
|
||||||
//if h as usize != note_axis {
|
|
||||||
//panic!("area height mismatch: {h} <> {note_axis}");
|
|
||||||
//}
|
|
||||||
for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
|
||||||
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
|
|
||||||
let source_x = time_start + area_x;
|
|
||||||
let source_y = note_hi - area_y;
|
|
||||||
// TODO: enable loop rollover:
|
|
||||||
//let source_x = (time_start + area_x) % source.width.max(1);
|
|
||||||
//let source_y = (note_hi - area_y) % source.height.max(1);
|
|
||||||
let is_in_x = source_x < source.width;
|
|
||||||
let is_in_y = source_y < source.height;
|
|
||||||
if is_in_x && is_in_y {
|
|
||||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
|
||||||
if let Some(cell) = to.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
|
|
||||||
*cell = source_cell.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(xywh)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cursor (&self) -> impl Draw<Tui> {
|
|
||||||
let note_hi = self.get_note_hi();
|
|
||||||
let note_lo = self.get_note_lo();
|
|
||||||
let note_pos = self.get_note_pos();
|
|
||||||
let note_len = self.get_note_len();
|
|
||||||
let time_pos = self.get_time_pos();
|
|
||||||
let time_start = self.get_time_start();
|
|
||||||
let time_zoom = self.get_time_zoom();
|
|
||||||
let style = Some(Style::default().fg(self.color.lightest.term));
|
|
||||||
thunk(move|to: &mut Tui|{
|
|
||||||
let xywh = to.area().into();
|
|
||||||
let XYWH(x0, y0, w, _h) = xywh;
|
|
||||||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
|
||||||
if note == note_pos {
|
|
||||||
for x in 0..w {
|
|
||||||
let screen_x = x0 + x;
|
|
||||||
let time_1 = time_start + x as usize * time_zoom;
|
|
||||||
let time_2 = time_1 + time_zoom;
|
|
||||||
if time_1 <= time_pos && time_pos < time_2 {
|
|
||||||
to.blit(&"█", screen_x, screen_y, style);
|
|
||||||
let tail = note_len as u16 / time_zoom as u16;
|
|
||||||
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
|
||||||
to.blit(&"▂", x_tail, screen_y, style);
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(xywh)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keys (&self) -> impl Draw<Tui> {
|
|
||||||
let state = self;
|
|
||||||
let color = state.color;
|
|
||||||
let note_lo = state.get_note_lo();
|
|
||||||
let note_hi = state.get_note_hi();
|
|
||||||
let note_pos = state.get_note_pos();
|
|
||||||
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
|
|
||||||
let off_style = Some(Style::default().fg(g(255)));
|
|
||||||
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold());
|
|
||||||
h_full(w_exact(self.keys_width, thunk(move|to: &mut Tui|{
|
|
||||||
let xywh = to.area().into();
|
|
||||||
let XYWH(x, y0, _w, _h) = xywh;
|
|
||||||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
|
||||||
to.blit(&to_key(note), x, screen_y, key_style);
|
|
||||||
if note > 127 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if note == note_pos {
|
|
||||||
to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style)
|
|
||||||
} else {
|
|
||||||
to.blit(¬e_pitch_to_name(note), x, screen_y, off_style)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(xywh)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timeline (&self) -> impl Draw<Tui> + '_ {
|
|
||||||
w_full(h_exact(1, thunk(move|to: &mut Tui|{
|
|
||||||
let xywh = to.area().into();
|
|
||||||
let XYWH(x, y, w, _h) = xywh;
|
|
||||||
let style = Some(Style::default().dim());
|
|
||||||
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
|
||||||
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
|
||||||
let t = area_x as usize * self.time_zoom().load(Relaxed);
|
|
||||||
if t < length {
|
|
||||||
to.blit(&"|", screen_x, y, style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(xywh)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { &self.clip }
|
|
||||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { &mut self.clip }
|
|
||||||
/// Determine the required space to render the clip.
|
|
||||||
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
|
|
||||||
(clip.length / self.range.time_zoom().load(Relaxed), 128)
|
|
||||||
}
|
|
||||||
fn redraw (&self) {
|
|
||||||
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
let buf_size = self.buffer_size(&clip);
|
|
||||||
let mut buffer = BigBuffer::from(buf_size);
|
|
||||||
let time_zoom = self.get_time_zoom();
|
|
||||||
self.time_len().store(clip.length, Relaxed);
|
|
||||||
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos());
|
|
||||||
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
|
|
||||||
buffer
|
|
||||||
} else {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
||||||
*self.clip_mut() = clip.cloned();
|
|
||||||
self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]);
|
|
||||||
self.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for PianoHorizontal {
|
|
||||||
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
||||||
let buffer = self.buffer.read().unwrap();
|
|
||||||
f.debug_struct("PianoHorizontal")
|
|
||||||
.field("time_zoom", &self.range.time_zoom)
|
|
||||||
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl OctaveVertical {
|
|
||||||
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 OctaveVertical {
|
|
||||||
fn tui (&self) -> impl Draw<Tui> {
|
|
||||||
east!(
|
|
||||||
fg_bg(self.color(0), self.color(1), "▙"),
|
|
||||||
fg_bg(self.color(2), self.color(3), "▙"),
|
|
||||||
fg_bg(self.color(4), self.color(5), "▌"),
|
|
||||||
fg_bg(self.color(6), self.color(7), "▟"),
|
|
||||||
fg_bg(self.color(8), self.color(9), "▟"),
|
|
||||||
fg_bg(self.color(10), self.color(11), "▟"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl_from!(MidiSelection: |data:(usize, bool)| Self {
|
|
||||||
time_len: Arc::new(0.into()),
|
|
||||||
note_axis: Arc::new(0.into()),
|
|
||||||
note_lo: Arc::new(0.into()),
|
|
||||||
time_axis: Arc::new(0.into()),
|
|
||||||
time_start: Arc::new(0.into()),
|
|
||||||
time_zoom: Arc::new(data.0.into()),
|
|
||||||
time_lock: Arc::new(data.1.into()),
|
|
||||||
});
|
|
||||||
impl_default!(MidiCursor: Self {
|
|
||||||
time_pos: Arc::new(0.into()),
|
|
||||||
note_pos: Arc::new(36.into()),
|
|
||||||
note_len: Arc::new(24.into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
impl NotePoint for MidiCursor {
|
|
||||||
fn note_len (&self) -> &AtomicUsize {
|
|
||||||
&self.note_len
|
|
||||||
}
|
|
||||||
fn note_pos (&self) -> &AtomicUsize {
|
|
||||||
&self.note_pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimePoint for MidiCursor {
|
|
||||||
fn time_pos (&self) -> &AtomicUsize {
|
|
||||||
self.time_pos.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeRange for MidiSelection {
|
|
||||||
fn time_len (&self) -> &AtomicUsize { &self.time_len }
|
|
||||||
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
|
|
||||||
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
|
|
||||||
fn time_start (&self) -> &AtomicUsize { &self.time_start }
|
|
||||||
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NoteRange for MidiSelection {
|
|
||||||
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
|
||||||
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
36
src/device/editor/octave.rs
Normal file
36
src/device/editor/octave.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
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 OctaveVertical {
|
||||||
|
pub fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
east!(
|
||||||
|
fg_bg(self.color(0), self.color(1), "▙"),
|
||||||
|
fg_bg(self.color(2), self.color(3), "▙"),
|
||||||
|
fg_bg(self.color(4), self.color(5), "▌"),
|
||||||
|
fg_bg(self.color(6), self.color(7), "▟"),
|
||||||
|
fg_bg(self.color(8), self.color(9), "▟"),
|
||||||
|
fg_bg(self.color(10), self.color(11), "▟"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_default!(OctaveVertical: Self {
|
||||||
|
on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
||||||
|
});
|
||||||
302
src/device/editor/piano.rs
Normal file
302
src/device/editor/piano.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A clip, rendered as a horizontal piano roll.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let piano = tek::PianoHorizontal::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
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: Size,
|
||||||
|
/// 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!(Size: |self: PianoHorizontal| self.size);
|
||||||
|
impl Draw<Tui> for PianoHorizontal {
|
||||||
|
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
self.tui().draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PianoHorizontal {
|
||||||
|
|
||||||
|
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
||||||
|
let size: Size = [0, 0].into();
|
||||||
|
let mut range = MidiSelection::from((12, true));
|
||||||
|
range.time_axis = size.x.clone();
|
||||||
|
range.note_axis = size.y.clone();
|
||||||
|
let piano = Self {
|
||||||
|
keys_width: 5,
|
||||||
|
size,
|
||||||
|
range,
|
||||||
|
buffer: RwLock::new(Default::default()).into(),
|
||||||
|
point: MidiCursor::default(),
|
||||||
|
clip: clip.cloned(),
|
||||||
|
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
|
||||||
|
};
|
||||||
|
piano.redraw();
|
||||||
|
piano
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
south(
|
||||||
|
east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()),
|
||||||
|
east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(self.cursor())))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the piano roll background.
|
||||||
|
///
|
||||||
|
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||||
|
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) {
|
||||||
|
for (y, note) in (0..=127).rev().enumerate() {
|
||||||
|
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
||||||
|
let cell = buf.get_mut(x, y).unwrap();
|
||||||
|
if note == (127-note_point) || time == time_point {
|
||||||
|
cell.set_bg(Rgb(0,0,0));
|
||||||
|
} else {
|
||||||
|
cell.set_bg(clip.color.darkest.term);
|
||||||
|
}
|
||||||
|
if time % 384 == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.term);
|
||||||
|
cell.set_char('│');
|
||||||
|
} else if time % 96 == 0 {
|
||||||
|
cell.set_fg(clip.color.dark.term);
|
||||||
|
cell.set_char('╎');
|
||||||
|
} else if time % note_len == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.term);
|
||||||
|
cell.set_char('┊');
|
||||||
|
} else if (127 - note) % 12 == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.term);
|
||||||
|
cell.set_char('=');
|
||||||
|
} else if (127 - note) % 6 == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.term);
|
||||||
|
cell.set_char('—');
|
||||||
|
} else {
|
||||||
|
cell.set_fg(clip.color.darker.term);
|
||||||
|
cell.set_char('·');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the piano roll foreground.
|
||||||
|
///
|
||||||
|
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||||
|
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
|
||||||
|
let style = Style::default().fg(clip.color.base.term);//.bg(Rgb(0, 0, 0));
|
||||||
|
let mut notes_on = [false;128];
|
||||||
|
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
|
||||||
|
for (_y, note) in (0..=127).rev().enumerate() {
|
||||||
|
if let Some(cell) = buf.get_mut(x, note) {
|
||||||
|
if notes_on[note] {
|
||||||
|
cell.set_char('▂');
|
||||||
|
cell.set_style(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let time_end = time_start + zoom;
|
||||||
|
for time in time_start..time_end.min(clip.length) {
|
||||||
|
for event in clip.notes[time].iter() {
|
||||||
|
match event {
|
||||||
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
|
let note = key.as_int() as usize;
|
||||||
|
if let Some(cell) = buf.get_mut(x, note) {
|
||||||
|
cell.set_char('█');
|
||||||
|
cell.set_style(style);
|
||||||
|
}
|
||||||
|
notes_on[note] = true
|
||||||
|
},
|
||||||
|
MidiMessage::NoteOff { key, .. } => {
|
||||||
|
notes_on[key.as_int() as usize] = false
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notes (&self) -> impl Draw<Tui> {
|
||||||
|
let time_start = self.get_time_start();
|
||||||
|
let note_lo = self.get_note_lo();
|
||||||
|
let note_hi = self.get_note_hi();
|
||||||
|
let buffer = self.buffer.clone();
|
||||||
|
thunk(move|to: &mut Tui|{
|
||||||
|
let xywh = to.area().into();
|
||||||
|
let XYWH(x0, y0, w, _h) = xywh;
|
||||||
|
let source = buffer.read().unwrap();
|
||||||
|
//if h as usize != note_axis {
|
||||||
|
//panic!("area height mismatch: {h} <> {note_axis}");
|
||||||
|
//}
|
||||||
|
for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
||||||
|
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
|
let source_x = time_start + area_x;
|
||||||
|
let source_y = note_hi - area_y;
|
||||||
|
// TODO: enable loop rollover:
|
||||||
|
//let source_x = (time_start + area_x) % source.width.max(1);
|
||||||
|
//let source_y = (note_hi - area_y) % source.height.max(1);
|
||||||
|
let is_in_x = source_x < source.width;
|
||||||
|
let is_in_y = source_y < source.height;
|
||||||
|
if is_in_x && is_in_y {
|
||||||
|
if let Some(source_cell) = source.get(source_x, source_y) {
|
||||||
|
if let Some(cell) = to.0.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
|
||||||
|
*cell = source_cell.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(xywh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor (&self) -> impl Draw<Tui> {
|
||||||
|
let note_hi = self.get_note_hi();
|
||||||
|
let note_lo = self.get_note_lo();
|
||||||
|
let note_pos = self.get_note_pos();
|
||||||
|
let note_len = self.get_note_len();
|
||||||
|
let time_pos = self.get_time_pos();
|
||||||
|
let time_start = self.get_time_start();
|
||||||
|
let time_zoom = self.get_time_zoom();
|
||||||
|
let style = Some(Style::default().fg(self.color.lightest.term));
|
||||||
|
thunk(move|to: &mut Tui|{
|
||||||
|
let xywh = to.area().into();
|
||||||
|
let XYWH(x0, y0, w, _h) = xywh;
|
||||||
|
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
|
if note == note_pos {
|
||||||
|
for x in 0..w {
|
||||||
|
let screen_x = x0 + x;
|
||||||
|
let time_1 = time_start + x as usize * time_zoom;
|
||||||
|
let time_2 = time_1 + time_zoom;
|
||||||
|
if time_1 <= time_pos && time_pos < time_2 {
|
||||||
|
to.blit(&"█", screen_x, screen_y, style);
|
||||||
|
let tail = note_len as u16 / time_zoom as u16;
|
||||||
|
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
||||||
|
to.blit(&"▂", x_tail, screen_y, style);
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(xywh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keys (&self) -> impl Draw<Tui> {
|
||||||
|
let state = self;
|
||||||
|
let color = state.color;
|
||||||
|
let note_lo = state.get_note_lo();
|
||||||
|
let note_hi = state.get_note_hi();
|
||||||
|
let note_pos = state.get_note_pos();
|
||||||
|
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
|
||||||
|
let off_style = Some(Style::default().fg(g(255)));
|
||||||
|
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.term).bold());
|
||||||
|
h_full(w_exact(self.keys_width, thunk(move|to: &mut Tui|{
|
||||||
|
let xywh = to.area().into();
|
||||||
|
let XYWH(x, y0, _w, _h) = xywh;
|
||||||
|
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
|
to.blit(&to_key(note), x, screen_y, key_style);
|
||||||
|
if note > 127 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if note == note_pos {
|
||||||
|
to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style)
|
||||||
|
} else {
|
||||||
|
to.blit(¬e_pitch_to_name(note), x, screen_y, off_style)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(xywh)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timeline (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
w_full(h_exact(1, thunk(move|to: &mut Tui|{
|
||||||
|
let xywh = to.area().into();
|
||||||
|
let XYWH(x, y, w, _h) = xywh;
|
||||||
|
let style = Some(Style::default().dim());
|
||||||
|
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||||
|
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
||||||
|
let t = area_x as usize * self.time_zoom().load(Relaxed);
|
||||||
|
if t < length {
|
||||||
|
to.blit(&"|", screen_x, y, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(xywh)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { &self.clip }
|
||||||
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { &mut self.clip }
|
||||||
|
/// Determine the required space to render the clip.
|
||||||
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
|
||||||
|
(clip.length / self.range.time_zoom().load(Relaxed), 128)
|
||||||
|
}
|
||||||
|
fn redraw (&self) {
|
||||||
|
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
let buf_size = self.buffer_size(&clip);
|
||||||
|
let mut buffer = BigBuffer::from(buf_size);
|
||||||
|
let time_zoom = self.get_time_zoom();
|
||||||
|
self.time_len().store(clip.length, Relaxed);
|
||||||
|
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos());
|
||||||
|
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||||
|
*self.clip_mut() = clip.cloned();
|
||||||
|
self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]);
|
||||||
|
self.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PianoHorizontal {
|
||||||
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
let buffer = self.buffer.read().unwrap();
|
||||||
|
f.debug_struct("PianoHorizontal")
|
||||||
|
.field("time_zoom", &self.range.time_zoom)
|
||||||
|
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/device/editor/point.rs
Normal file
76
src/device/editor/point.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::MidiCursor::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)] pub struct MidiCursor {
|
||||||
|
/// Time coordinate of cursor
|
||||||
|
pub time_pos: Arc<AtomicUsize>,
|
||||||
|
/// Note coordinate of cursor
|
||||||
|
pub note_pos: Arc<AtomicUsize>,
|
||||||
|
/// Length of note that will be inserted, in pulses
|
||||||
|
pub note_len: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_default!(MidiCursor: Self {
|
||||||
|
time_pos: Arc::new(0.into()),
|
||||||
|
note_pos: Arc::new(36.into()),
|
||||||
|
note_len: Arc::new(24.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl NotePoint for MidiCursor {
|
||||||
|
fn note_len (&self) -> &AtomicUsize {
|
||||||
|
&self.note_len
|
||||||
|
}
|
||||||
|
fn note_pos (&self) -> &AtomicUsize {
|
||||||
|
&self.note_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for MidiCursor {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize {
|
||||||
|
self.time_pos.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NotePoint {
|
||||||
|
fn note_len (&self) -> &AtomicUsize;
|
||||||
|
/// Get the current length of the note cursor.
|
||||||
|
fn get_note_len (&self) -> usize {
|
||||||
|
self.note_len().load(Relaxed)
|
||||||
|
}
|
||||||
|
/// Set the length of the note cursor, returning the previous value.
|
||||||
|
fn set_note_len (&self, x: usize) -> usize {
|
||||||
|
self.note_len().swap(x, Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_pos (&self) -> &AtomicUsize;
|
||||||
|
/// Get the current pitch of the note cursor.
|
||||||
|
fn get_note_pos (&self) -> usize {
|
||||||
|
self.note_pos().load(Relaxed).min(127)
|
||||||
|
}
|
||||||
|
/// Set the current pitch fo the note cursor, returning the previous value.
|
||||||
|
fn set_note_pos (&self, x: usize) -> usize {
|
||||||
|
self.note_pos().swap(x.min(127), Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimePoint {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize;
|
||||||
|
/// Get the current time position of the note cursor.
|
||||||
|
fn get_time_pos (&self) -> usize {
|
||||||
|
self.time_pos().load(Relaxed)
|
||||||
|
}
|
||||||
|
/// Set the current time position of the note cursor, returning the previous value.
|
||||||
|
fn set_time_pos (&self, x: usize) -> usize {
|
||||||
|
self.time_pos().swap(x, Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiPoint: NotePoint + TimePoint {
|
||||||
|
/// Get the current end of the note cursor.
|
||||||
|
fn get_note_end (&self) -> usize {
|
||||||
|
self.get_time_pos() + self.get_note_len()
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/device/editor/range.rs
Normal file
124
src/device/editor/range.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tek::{TimeRange, NoteRange};
|
||||||
|
/// let model = tek::MidiSelection::from((1, false));
|
||||||
|
///
|
||||||
|
/// let _ = model.get_time_len();
|
||||||
|
/// let _ = model.get_time_zoom();
|
||||||
|
/// let _ = model.get_time_lock();
|
||||||
|
/// let _ = model.get_time_start();
|
||||||
|
/// let _ = model.get_time_axis();
|
||||||
|
/// let _ = model.get_time_end();
|
||||||
|
///
|
||||||
|
/// let _ = model.get_note_lo();
|
||||||
|
/// let _ = model.get_note_axis();
|
||||||
|
/// let _ = model.get_note_hi();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
|
||||||
|
pub time_len: Arc<AtomicUsize>,
|
||||||
|
/// Length of visible time axis
|
||||||
|
pub time_axis: Arc<AtomicUsize>,
|
||||||
|
/// Earliest time displayed
|
||||||
|
pub time_start: Arc<AtomicUsize>,
|
||||||
|
/// Time step
|
||||||
|
pub time_zoom: Arc<AtomicUsize>,
|
||||||
|
/// Auto rezoom to fit in time axis
|
||||||
|
pub time_lock: Arc<AtomicBool>,
|
||||||
|
/// Length of visible note axis
|
||||||
|
pub note_axis: Arc<AtomicUsize>,
|
||||||
|
// Lowest note displayed
|
||||||
|
pub note_lo: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiRange: TimeRange + NoteRange {}
|
||||||
|
|
||||||
|
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_from!(MidiSelection: |data:(usize, bool)| Self {
|
||||||
|
time_len: Arc::new(0.into()),
|
||||||
|
note_axis: Arc::new(0.into()),
|
||||||
|
note_lo: Arc::new(0.into()),
|
||||||
|
time_axis: Arc::new(0.into()),
|
||||||
|
time_start: Arc::new(0.into()),
|
||||||
|
time_zoom: Arc::new(data.0.into()),
|
||||||
|
time_lock: Arc::new(data.1.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl TimeRange for MidiSelection {
|
||||||
|
fn time_len (&self) -> &AtomicUsize { &self.time_len }
|
||||||
|
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
|
||||||
|
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
|
||||||
|
fn time_start (&self) -> &AtomicUsize { &self.time_start }
|
||||||
|
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteRange for MidiSelection {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
||||||
|
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimeRange {
|
||||||
|
fn time_len (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_len (&self) -> usize {
|
||||||
|
self.time_len().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn time_zoom (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_zoom (&self) -> usize {
|
||||||
|
self.time_zoom().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn set_time_zoom (&self, value: usize) -> usize {
|
||||||
|
self.time_zoom().swap(value, Relaxed)
|
||||||
|
}
|
||||||
|
fn time_lock (&self) -> &AtomicBool;
|
||||||
|
fn get_time_lock (&self) -> bool {
|
||||||
|
self.time_lock().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn set_time_lock (&self, value: bool) -> bool {
|
||||||
|
self.time_lock().swap(value, Relaxed)
|
||||||
|
}
|
||||||
|
fn time_start (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_start (&self) -> usize {
|
||||||
|
self.time_start().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn set_time_start (&self, value: usize) -> usize {
|
||||||
|
self.time_start().swap(value, Relaxed)
|
||||||
|
}
|
||||||
|
fn time_axis (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_axis (&self) -> usize {
|
||||||
|
self.time_axis().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn get_time_end (&self) -> usize {
|
||||||
|
self.time_start().load(Relaxed) + self.time_axis().load(Relaxed) * self.time_zoom().load(Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NoteRange {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize;
|
||||||
|
fn note_axis (&self) -> &AtomicUsize;
|
||||||
|
|
||||||
|
fn get_note_lo (&self) -> usize {
|
||||||
|
self.note_lo().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn set_note_lo (&self, x: usize) -> usize {
|
||||||
|
self.note_lo().swap(x, Relaxed)
|
||||||
|
}
|
||||||
|
fn get_note_axis (&self) -> usize {
|
||||||
|
self.note_axis().load(Relaxed)
|
||||||
|
}
|
||||||
|
fn get_note_hi (&self) -> usize {
|
||||||
|
(self.get_note_lo() + self.get_note_axis().saturating_sub(1)).min(127)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/device/meter.rs
Normal file
84
src/device/meter.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||||
|
#[default] Rms,
|
||||||
|
Log10,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Log10Meter(pub f32);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct RmsMeter(pub f32);
|
||||||
|
|
||||||
|
impl Draw<Tui> for RmsMeter {
|
||||||
|
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
let XYWH(x, y, w, h) = to.area().into();
|
||||||
|
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||||
|
let v = (signal * h as f32).ceil() as u16;
|
||||||
|
let y2 = y + h;
|
||||||
|
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
|
||||||
|
for y in y..(y + v) {
|
||||||
|
for x in x..(x + w) {
|
||||||
|
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(to.area().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<Tui> for Log10Meter {
|
||||||
|
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
let XYWH(x, y, w, h) = to.area().into();
|
||||||
|
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||||
|
let v = (signal * h as f32 / 100.0).ceil() as u16;
|
||||||
|
let y2 = y + h;
|
||||||
|
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
|
||||||
|
for y in y..(y + v) {
|
||||||
|
for x in x..(x + w) {
|
||||||
|
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(to.area().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
|
||||||
|
bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{
|
||||||
|
h_full(RmsMeter(*value))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||||||
|
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||||||
|
let count = samples.len() as f32;
|
||||||
|
10. * (total / count).log10()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_rms (samples: &[f32]) -> f32 {
|
||||||
|
let sum = samples.iter()
|
||||||
|
.map(|s|*s)
|
||||||
|
.reduce(|sum, sample|sum + sample.abs())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
(sum / samples.len() as f32).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] mod test_view_meter {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
proptest! {
|
||||||
|
|
||||||
|
#[test] fn proptest_view_meter (
|
||||||
|
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||||
|
) {
|
||||||
|
let _ = view_meter(&label, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test] fn proptest_view_meters (
|
||||||
|
value1 in f32::MIN..f32::MAX,
|
||||||
|
value2 in f32::MIN..f32::MAX
|
||||||
|
) {
|
||||||
|
let _ = view_meters(&[value1, value2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,78 +1,11 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
|
||||||
#[default] Rms,
|
|
||||||
Log10,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct Log10Meter(pub f32);
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct RmsMeter(pub f32);
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub enum MixingMode {
|
pub enum MixingMode {
|
||||||
#[default] Summing,
|
#[default] Summing,
|
||||||
Average,
|
Average,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)] mod test_view_meter {
|
|
||||||
use super::*;
|
|
||||||
use proptest::prelude::*;
|
|
||||||
proptest! {
|
|
||||||
|
|
||||||
#[test] fn proptest_view_meter (
|
|
||||||
label in "\\PC*", value in f32::MIN..f32::MAX
|
|
||||||
) {
|
|
||||||
let _ = view_meter(&label, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test] fn proptest_view_meters (
|
|
||||||
value1 in f32::MIN..f32::MAX,
|
|
||||||
value2 in f32::MIN..f32::MAX
|
|
||||||
) {
|
|
||||||
let _ = view_meters(&[value1, value2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<Tui> for RmsMeter {
|
|
||||||
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
|
||||||
let XYWH(x, y, w, h) = to.area();
|
|
||||||
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
|
|
||||||
let v = (signal * h as f32).ceil() as u16;
|
|
||||||
let y2 = y + h;
|
|
||||||
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
|
|
||||||
for y in y..(y + v) {
|
|
||||||
for x in x..(x + w) {
|
|
||||||
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Draw<Tui> for Log10Meter {
|
|
||||||
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
|
||||||
let XYWH(x, y, w, h) = to.area();
|
|
||||||
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
|
|
||||||
let v = (signal * h as f32 / 100.0).ceil() as u16;
|
|
||||||
let y2 = y + h;
|
|
||||||
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
|
|
||||||
for y in y..(y + v) {
|
|
||||||
for x in x..(x + w) {
|
|
||||||
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
|
|
||||||
bg(Black, w_exact(2, iter_east_fixed(1, ||meters.iter(), |value, _index|{
|
|
||||||
h_full(RmsMeter(*value))
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mix_summing <const N: usize> (
|
pub fn mix_summing <const N: usize> (
|
||||||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
|
@ -107,18 +40,3 @@ pub fn mix_average <const N: usize> (
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_log10 (samples: &[f32]) -> f32 {
|
|
||||||
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
|
||||||
let count = samples.len() as f32;
|
|
||||||
10. * (total / count).log10()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn to_rms (samples: &[f32]) -> f32 {
|
|
||||||
let sum = samples.iter()
|
|
||||||
.map(|s|*s)
|
|
||||||
.reduce(|sum, sample|sum + sample.abs())
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
(sum / samples.len() as f32).sqrt()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,10 @@ pub(crate) use tengri::{
|
||||||
/// Command-line entrypoint.
|
/// Command-line entrypoint.
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
pub fn main () -> Usually<()> {
|
pub fn main () -> Usually<()> {
|
||||||
|
|
||||||
Config::watch(|config|{
|
Config::watch(|config|{
|
||||||
Exit::run(|exit|{
|
Exit::run(|exit|{
|
||||||
Jack::new_run("tek", |jack|{
|
Jack::new_run("tek", move|jack|{
|
||||||
App::new_shared_run(&exit, &jack, Arrangement::new(
|
Ok(App::new_shared_run(&exit, &jack, Arrangement::new(
|
||||||
&jack,
|
&jack,
|
||||||
None,
|
None,
|
||||||
Clock::new(&jack, Some(51.))?,
|
Clock::new(&jack, Some(51.))?,
|
||||||
|
|
@ -99,13 +98,12 @@ pub fn main () -> Usually<()> {
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
), config, ":menu")
|
), config, ":menu")?.0)
|
||||||
// TODO: Sync I/O timings with main clock, so that things
|
// TODO: Sync I/O timings with main clock, so that things
|
||||||
// "accidentally" fall on the beat in overload conditions.
|
// "accidentally" fall on the beat in overload conditions.
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue