simplifying phrase editor

This commit is contained in:
🪞👃🪞 2024-12-12 23:48:33 +01:00
parent 9619ef9739
commit 46467d3972
3 changed files with 261 additions and 256 deletions

View file

@ -66,31 +66,17 @@ impl Audio for SequencerTui {
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if ClockAudio(self)
.process(client, scope) == Control::Quit
{
if Control::Quit == ClockAudio(self).process(client, scope) {
return Control::Quit
}
// Update MIDI sequencer
if PlayerAudio(&mut self.player, &mut self.note_buf, &mut self.midi_buf)
.process(client, scope) == Control::Quit
{
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
// End profiling cycle
self.perf.update(t0, scope);
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now().set(now);
//}
//}
Control::Continue
}
}

View file

@ -6,201 +6,6 @@ pub trait HasEditor {
fn editor_entered (&self) -> bool;
}
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub(crate) phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
/// Renders the phrase
pub(crate) view_mode: Box<dyn PhraseViewMode>,
// Lowest note displayed
pub(crate) note_lo: AtomicUsize,
/// Note coordinate of cursor
pub(crate) note_point: AtomicUsize,
/// Length of note that will be inserted, in pulses
pub(crate) note_len: Arc<AtomicUsize>,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// Earliest time displayed
pub(crate) time_start: AtomicUsize,
/// Time coordinate of cursor
pub(crate) time_point: AtomicUsize,
/// Current position of global playhead
pub(crate) now: Arc<Pulse>,
/// Width and height of notes area at last render
pub(crate) size: Measure<Tui>,
}
impl From<&Arc<RwLock<Phrase>>> for PhraseEditorModel {
fn from (phrase: &Arc<RwLock<Phrase>>) -> Self {
Self::from(Some(phrase.clone()))
}
}
impl From<Option<Arc<RwLock<Phrase>>>> for PhraseEditorModel {
fn from (phrase: Option<Arc<RwLock<Phrase>>>) -> Self {
let model = Self::default();
*model.phrase.write().unwrap() = phrase;
model
}
}
impl std::fmt::Debug for PhraseEditorModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PhraseEditorModel")
.field("note_axis", &format!("{} {}",
self.note_lo.load(Ordering::Relaxed),
self.note_point.load(Ordering::Relaxed),
))
.field("time_axis", &format!("{} {}",
self.time_start.load(Ordering::Relaxed),
self.time_point.load(Ordering::Relaxed),
))
.finish()
}
}
impl Default for PhraseEditorModel {
fn default () -> Self {
let phrase = Arc::new(RwLock::new(None));
let note_len = Arc::from(AtomicUsize::from(24));
Self {
size: Measure::new(),
phrase: phrase.clone(),
now: Pulse::default().into(),
time_start: 0.into(),
time_point: 0.into(),
note_lo: 0.into(),
note_point: 0.into(),
note_len: note_len.clone(),
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
view_mode: Box::new(PianoHorizontal {
phrase: Arc::new(RwLock::new(None)),
buffer: Default::default(),
time_zoom: 24,
time_lock: true,
note_zoom: PhraseViewNoteZoom::N(1),
focused: true,
note_len
}),
}
}
}
impl PhraseEditorModel {
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
*self.view_mode.phrase().write().unwrap() = if phrase.is_some() {
phrase.clone()
} else {
None
};
self.view_mode.redraw();
}
/// Put note at current position
pub fn put_note (&mut self, advance: bool) {
if let Some(phrase) = &*self.phrase.read().unwrap() {
let note_len = self.note_len.load(Ordering::Relaxed);
let time = self.time_point.load(Ordering::Relaxed);
let note = self.note_point.load(Ordering::Relaxed);
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from(note as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.view_mode.redraw();
if advance {
let point = self.time_point.load(Ordering::Relaxed);
let length = phrase.length;
let forward = |time|(time + note_len) % length;
self.time_point.store(forward(point), Ordering::Relaxed);
}
}
}
}
render!(|self: PhraseEditorModel|self.view_mode);
pub trait PhraseViewMode: Render<Tui> + Debug + Send + Sync {
fn time_zoom (&self) -> usize;
fn set_time_zoom (&mut self, time_zoom: usize);
fn time_zoom_lock (&self) -> bool;
fn set_time_zoom_lock (&mut self, time_zoom: bool);
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize);
fn redraw (&mut self);
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>;
}
impl PhraseViewMode for PhraseEditorModel {
fn time_zoom (&self) -> usize {
self.view_mode.time_zoom()
}
fn set_time_zoom (&mut self, time_zoom: usize) {
self.view_mode.set_time_zoom(time_zoom)
}
fn time_zoom_lock (&self) -> bool {
self.view_mode.time_zoom_lock()
}
fn set_time_zoom_lock (&mut self, time_lock: bool) {
self.view_mode.set_time_zoom_lock(time_lock);
}
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) {
self.view_mode.buffer_size(phrase)
}
fn redraw (&mut self) {
self.view_mode.redraw()
}
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>> {
self.view_mode.phrase()
}
}
pub struct PhraseView<'a> {
note_point: usize,
note_range: (usize, usize),
time_start: usize,
time_point: usize,
note_len: usize,
phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
view_mode: &'a Box<dyn PhraseViewMode>,
now: &'a Arc<Pulse>,
size: &'a Measure<Tui>,
focused: bool,
entered: bool,
}
impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
fn from (state: &'a T) -> Self {
let editor = state.editor();
let height = editor.size.h();
let note_point = editor.note_point.load(Ordering::Relaxed);
let mut note_lo = editor.note_lo.load(Ordering::Relaxed);
let mut note_hi = 127.min((note_lo + height).saturating_sub(2));
if note_point > note_hi {
note_lo += note_point - note_hi;
note_hi = note_point;
editor.note_lo.store(note_lo, Ordering::Relaxed);
}
Self {
note_point,
note_range: (note_lo, note_hi),
time_start: editor.time_start.load(Ordering::Relaxed),
time_point: editor.time_point.load(Ordering::Relaxed),
note_len: editor.note_len.load(Ordering::Relaxed),
phrase: editor.phrase.clone(),
view_mode: &editor.view_mode,
size: &editor.size,
now: &editor.now,
focused: state.editor_focused(),
entered: state.editor_entered(),
}
}
}
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
@ -220,18 +25,18 @@ pub enum PhraseCommand {
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
let note_lo = state.note_lo.load(Ordering::Relaxed);
let note_point = state.note_point.load(Ordering::Relaxed);
let time_start = state.time_start.load(Ordering::Relaxed);
let time_point = state.time_point.load(Ordering::Relaxed);
let time_zoom = state.view_mode.time_zoom();
use KeyCode::{Char, Up, Down, PageUp, PageDown, Left, Right};
let note_lo = state.range.note_lo.load(Ordering::Relaxed);
let note_point = state.point.note_point.load(Ordering::Relaxed);
let time_start = state.range.time_start.load(Ordering::Relaxed);
let time_point = state.point.time_point.load(Ordering::Relaxed);
let time_zoom = state.mode.time_zoom();
let length = state.phrase().read().unwrap().as_ref()
.map(|p|p.read().unwrap().length).unwrap_or(1);
let note_len = state.note_len.load(Ordering::Relaxed);
let note_len = state.point.note_len.load(Ordering::Relaxed);
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Char('z')) => SetTimeZoomLock(!state.view_mode.time_zoom_lock()),
key!(Char('z')) => SetTimeZoomLock(!state.mode.time_zoom_lock()),
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
@ -271,21 +76,21 @@ impl Command<PhraseEditorModel> for PhraseCommand {
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
use PhraseCommand::*;
match self {
Show(phrase) => { state.show_phrase(phrase); },
Show(phrase) => { state.set_phrase(phrase); },
PutNote => { state.put_note(false); },
AppendNote => { state.put_note(true); },
SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); },
SetTimeZoomLock(lock) => { state.view_mode.set_time_zoom_lock(lock); },
SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); },
SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); },
SetNoteLength(time) => { state.note_len.store(time, Ordering::Relaxed); },
SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); },
SetTimeZoom(zoom) => { state.mode.set_time_zoom(zoom); },
SetTimeZoomLock(lock) => { state.mode.set_time_zoom_lock(lock); },
SetTimeScroll(time) => { state.range.time_start.store(time, Ordering::Relaxed); },
SetTimeCursor(time) => { state.point.time_point.store(time, Ordering::Relaxed); },
SetNoteLength(time) => { state.point.note_len.store(time, Ordering::Relaxed); },
SetNoteScroll(note) => { state.range.note_lo.store(note, Ordering::Relaxed); },
SetNoteCursor(note) => {
let note = 127.min(note);
let start = state.note_lo.load(Ordering::Relaxed);
state.note_point.store(note, Ordering::Relaxed);
let start = state.range.note_lo.load(Ordering::Relaxed);
state.point.note_point.store(note, Ordering::Relaxed);
if note < start {
state.note_lo.store(note, Ordering::Relaxed);
state.range.note_lo.store(note, Ordering::Relaxed);
}
},
@ -294,3 +99,191 @@ impl Command<PhraseEditorModel> for PhraseCommand {
Ok(None)
}
}
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
/// Renders the phrase
pub mode: Box<dyn PhraseViewMode>,
/// The display window
pub range: PhraseEditorRange,
/// The note cursor
pub point: PhraseEditorPoint,
}
impl Default for PhraseEditorModel {
fn default () -> Self {
let phrase = Arc::new(RwLock::new(None));
let range = PhraseEditorRange::default();
let point = PhraseEditorPoint::default();
let mode = PianoHorizontal::new(&phrase, &range, &point);
Self { phrase, mode: Box::new(mode), range, point }
}
}
render!(|self: PhraseEditorModel|self.mode);
pub trait PhraseViewMode: Render<Tui> + Debug + Send + Sync {
fn time_zoom (&self) -> usize;
fn set_time_zoom (&mut self, time_zoom: usize);
fn time_zoom_lock (&self) -> bool;
fn set_time_zoom_lock (&mut self, time_zoom: bool);
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize);
fn redraw (&mut self);
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>;
fn set_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
*self.phrase().write().unwrap() = phrase;
self.redraw();
}
}
impl PhraseViewMode for PhraseEditorModel {
fn time_zoom (&self) -> usize {
self.mode.time_zoom()
}
fn set_time_zoom (&mut self, time_zoom: usize) {
self.mode.set_time_zoom(time_zoom)
}
fn time_zoom_lock (&self) -> bool {
self.mode.time_zoom_lock()
}
fn set_time_zoom_lock (&mut self, time_lock: bool) {
self.mode.set_time_zoom_lock(time_lock);
}
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) {
self.mode.buffer_size(phrase)
}
fn redraw (&mut self) {
self.mode.redraw()
}
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>> {
self.mode.phrase()
}
}
#[derive(Debug, Clone)]
pub struct PhraseEditorRange {
/// Earliest time displayed
pub time_start: Arc<AtomicUsize>,
/// Time step
pub time_zoom: Arc<AtomicUsize>,
// Lowest note displayed
pub note_lo: Arc<AtomicUsize>,
}
impl Default for PhraseEditorRange {
fn default () -> Self {
Self {
time_start: Arc::new(0.into()),
time_zoom: Arc::new(24.into()),
note_lo: Arc::new(0.into()),
}
}
}
#[derive(Debug, Clone)]
pub struct PhraseEditorPoint {
/// Time coordinate of cursor
pub time_point: Arc<AtomicUsize>,
/// Note coordinate of cursor
pub note_point: Arc<AtomicUsize>,
/// Length of note that will be inserted, in pulses
pub note_len: Arc<AtomicUsize>,
}
impl Default for PhraseEditorPoint {
fn default () -> Self {
Self {
time_point: Arc::new(0.into()),
note_point: Arc::new(0.into()),
note_len: Arc::new(24.into()),
}
}
}
impl PhraseEditorModel {
/// Put note at current position
pub fn put_note (&mut self, advance: bool) {
if let Some(phrase) = &*self.phrase.read().unwrap() {
let note_len = self.point.note_len.load(Ordering::Relaxed);
let time = self.point.time_point.load(Ordering::Relaxed);
let note = self.point.note_point.load(Ordering::Relaxed);
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from(note as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.mode.redraw();
if advance {
let point = self.point.time_point.load(Ordering::Relaxed);
let length = phrase.length;
let forward = |time|(time + note_len) % length;
self.point.time_point.store(forward(point), Ordering::Relaxed);
}
}
}
}
impl From<&Arc<RwLock<Phrase>>> for PhraseEditorModel {
fn from (phrase: &Arc<RwLock<Phrase>>) -> Self {
Self::from(Some(phrase.clone()))
}
}
impl From<Option<Arc<RwLock<Phrase>>>> for PhraseEditorModel {
fn from (phrase: Option<Arc<RwLock<Phrase>>>) -> Self {
let model = Self::default();
*model.phrase.write().unwrap() = phrase;
model
}
}
impl std::fmt::Debug for PhraseEditorModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PhraseEditorModel")
.field("range", &self.range)
.field("point", &self.point)
.finish()
}
}
fn autoscroll_notes (
range: &PhraseEditorRange, point: &PhraseEditorPoint, height: usize
) -> (usize, (usize, usize)) {
let note_point = point.note_point.load(Ordering::Relaxed);
let mut note_lo = range.note_lo.load(Ordering::Relaxed);
let mut note_hi = 127.min((note_lo + height).saturating_sub(2));
if note_point > note_hi {
note_lo += note_point - note_hi;
note_hi = note_point;
range.note_lo.store(note_lo, Ordering::Relaxed);
}
(note_point, (note_lo, note_hi))
}
//impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
//fn from (state: &'a T) -> Self {
//let editor = state.editor();
//let (note_point, note_range) = autoscroll_notes(
//&editor.range,
//&editor.point,
//editor.size.h()
//);
//Self {
//note_point,
//note_range,
//time_start: editor.range.time_start.load(Ordering::Relaxed),
//time_point: editor.point.time_point.load(Ordering::Relaxed),
//note_len: editor.point.note_len.load(Ordering::Relaxed),
//phrase: editor.phrase.clone(),
//mode: &editor.mode,
//size: &editor.size,
//now: &editor.now,
//focused: state.editor_focused(),
//entered: state.editor_entered(),
//}
//}
//}

View file

@ -1,28 +1,42 @@
use crate::*;
use super::*;
/// A phrase, rendered as a horizontal piano roll.
pub struct PianoHorizontal {
pub(crate) phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
pub(crate) time_lock: bool,
pub(crate) time_zoom: usize,
pub(crate) note_zoom: PhraseViewNoteZoom,
pub(crate) note_len: Arc<AtomicUsize>,
pub(crate) buffer: BigBuffer,
pub(crate) focused: bool,
phrase: Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
time_lock: bool,
time_zoom: Arc<AtomicUsize>,
note_len: Arc<AtomicUsize>,
buffer: BigBuffer,
/// Width and height of notes area at last render
size: Measure<Tui>,
}
#[derive(Copy, Clone, Debug)]
pub enum PhraseViewNoteZoom {
N(usize),
Half,
Octant,
impl PianoHorizontal {
pub fn new (
phrase: &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>>,
range: &PhraseEditorRange,
point: &PhraseEditorPoint,
) -> Self {
Self {
phrase: phrase.clone(),
buffer: Default::default(),
time_lock: true,
time_zoom: range.time_zoom.clone(),
note_len: point.note_len.clone(),
size: Measure::new()
}
}
}
render!(|self: PianoHorizontal|{
let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset };
let bg = TuiTheme::g(32);
let fg = self.phrase().read().unwrap()
.as_ref().map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
Tui::bg(bg, Tui::split_up(false, 2,
Tui::bg(bg, Tui::split_down(false, 1,
Tui::bg(fg.dark.rgb, PianoHorizontalTimeline {
start: "TIMELINE".into()
start: "|0".into()
}),
Split::right(false, 5, PianoHorizontalKeys {
color: ItemPalette::random(),
@ -30,6 +44,7 @@ render!(|self: PianoHorizontal|{
note_hi: 0,
note_point: None
}, lay!([
self.size,
PianoHorizontalNotes {
source: &self.buffer,
time_start: 0,
@ -47,12 +62,14 @@ render!(|self: PianoHorizontal|{
])),
))
});
pub struct PianoHorizontalTimeline {
start: String
}
render!(|self: PianoHorizontalTimeline|{
Tui::fg(TuiTheme::g(224), Tui::push_x(5, self.start.as_str()))
});
pub struct PianoHorizontalKeys {
color: ItemPalette,
note_lo: usize,
@ -89,6 +106,7 @@ render!(|self: PianoHorizontalKeys|render(|to|Ok({
};
}
})));
pub struct PianoHorizontalCursor {
time_zoom: usize,
time_point: usize,
@ -119,6 +137,7 @@ render!(|self: PianoHorizontalCursor|render(|to|Ok({
}
}
})));
pub struct PianoHorizontalNotes<'a> {
source: &'a BigBuffer,
time_start: usize,
@ -146,6 +165,7 @@ render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({
}
}
})));
impl PianoHorizontal {
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) {
@ -204,15 +224,16 @@ impl PianoHorizontal {
}
}
}
impl PhraseViewMode for PianoHorizontal {
fn phrase (&self) -> &Arc<RwLock<Option<Arc<RwLock<Phrase>>>>> {
&self.phrase
}
fn time_zoom (&self) -> usize {
self.time_zoom
self.time_zoom.load(Ordering::Relaxed)
}
fn set_time_zoom (&mut self, time_zoom: usize) {
self.time_zoom = time_zoom;
self.time_zoom.store(time_zoom, Ordering::Relaxed);
self.redraw()
}
fn time_zoom_lock (&self) -> bool {
@ -224,13 +245,7 @@ impl PhraseViewMode for PianoHorizontal {
}
/// Determine the required space to render the phrase.
fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) {
let width = phrase.length / self.time_zoom();
let height = match self.note_zoom {
PhraseViewNoteZoom::Half => 64,
PhraseViewNoteZoom::N(n) => 128*n,
_ => unimplemented!()
};
(width, height)
(phrase.length / self.time_zoom(), 128)
}
fn redraw (&mut self) {
let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() {
@ -246,11 +261,11 @@ impl PhraseViewMode for PianoHorizontal {
self.buffer = buffer
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.time_zoom)
.field("note_zoom", &self.note_zoom)
.field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height))
.finish()
}
@ -287,3 +302,14 @@ impl std::fmt::Debug for PianoHorizontal {
//}
//}
//}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now().set(now);
//}
//}