reenabling phrase editing

This commit is contained in:
🪞👃🪞 2024-12-02 02:44:52 +01:00
parent 37d4a42a83
commit 82a326d6f8
6 changed files with 95 additions and 77 deletions

View file

@ -97,7 +97,7 @@ pub fn to_focus_command (input: &TuiInput) -> Option<FocusCommand> {
} }
#[macro_export] macro_rules! impl_focus { #[macro_export] macro_rules! impl_focus {
($Struct:ident $Focus:ident $Grid:expr) => { ($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
impl HasFocus for $Struct { impl HasFocus for $Struct {
type Item = $Focus; type Item = $Focus;
/// Get the currently focused item. /// Get the currently focused item.
@ -108,6 +108,7 @@ pub fn to_focus_command (input: &TuiInput) -> Option<FocusCommand> {
fn set_focused (&mut self, to: Self::Item) { fn set_focused (&mut self, to: Self::Item) {
self.focus.set_inner(to) self.focus.set_inner(to)
} }
$(fn focus_updated (&mut $self) { $update_focus })?
} }
impl HasEnter for $Struct { impl HasEnter for $Struct {
/// Get the currently focused item. /// Get the currently focused item.

View file

@ -104,7 +104,13 @@ impl_focus!(SequencerTui SequencerFocus [
PhraseEditor, PhraseEditor,
PhraseEditor, PhraseEditor,
], ],
]); ] => [self: {
if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor {
self.editor.edit_mode = PhraseEditMode::Note
} else {
self.editor.edit_mode = PhraseEditMode::Scroll
}
}]);
/// Status bar for sequencer app /// Status bar for sequencer app
#[derive(Clone)] #[derive(Clone)]
@ -144,10 +150,13 @@ impl From<&SequencerTui> for SequencerStatusBar {
Transport(Sync) => " LAUNCH SYNC ", Transport(Sync) => " LAUNCH SYNC ",
Transport(Quant) => " REC QUANT ", Transport(Quant) => " REC QUANT ",
Transport(Clock) => " SEEK ", Transport(Clock) => " SEEK ",
PhraseList => " PHRASES ",
PhraseEditor => " EDIT MIDI ",
PhrasePlay => " TO PLAY ", PhrasePlay => " TO PLAY ",
PhraseNext => " UP NEXT ", PhraseNext => " UP NEXT ",
PhraseList => " PHRASES ",
PhraseEditor => match state.editor.edit_mode {
PhraseEditMode::Note => " EDIT MIDI ",
PhraseEditMode::Scroll => " VIEW MIDI ",
},
}, },
help: match state.focused() { help: match state.focused() {
Transport(PlayPause) => &[ Transport(PlayPause) => &[
@ -164,6 +173,10 @@ impl From<&SequencerTui> for SequencerStatusBar {
Transport(Quant) => &[ Transport(Quant) => &[
("", ".,", " inc/dec"), ("", ".,", " inc/dec"),
], ],
Transport(Clock) => &[
("", ".,", " by beat"),
("", "<>", " by time"),
],
PhraseList => if state.entered() { PhraseList => if state.entered() {
&[ &[
("", "", " pick"), ("", "", " pick"),
@ -173,6 +186,14 @@ impl From<&SequencerTui> for SequencerStatusBar {
] ]
} else { } else {
default_help default_help
},
PhraseEditor => match state.editor.edit_mode {
PhraseEditMode::Note => &[
("", "", " cursor"),
],
PhraseEditMode::Scroll => &[
("", "", " scroll"),
],
} }
_ => if state.entered() { _ => if state.entered() {
&[ &[

View file

@ -33,29 +33,30 @@ impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
key!(Char('_')) => SetTimeZoom(next_note_length(time_scale)), key!(Char('_')) => SetTimeZoom(next_note_length(time_scale)),
key!(Char('=')) => SetTimeZoom(prev_note_length(time_scale)), key!(Char('=')) => SetTimeZoom(prev_note_length(time_scale)),
key!(Char('+')) => SetTimeZoom(prev_note_length(time_scale)), key!(Char('+')) => SetTimeZoom(prev_note_length(time_scale)),
key!(Char('a')) => AppendNote,
key!(Char('s')) => PutNote,
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
key!(Char('n')) => { todo!("toggle keys vs notes") },
_ => match state.edit_mode { _ => match state.edit_mode {
PhraseEditMode::Scroll => match from.event() { PhraseEditMode::Scroll => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Note), key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
key!(Up) => SetNoteCursor(note_point + 1),
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key!(PageUp) => SetNoteCursor(note_point + 3),
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_scale)),
key!(Right) => SetTimeCursor((time_point + time_scale) % length),
_ => return None
},
PhraseEditMode::Note => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
key!(Up) => SetNoteScroll(note_lo + 1), key!(Up) => SetNoteScroll(note_lo + 1),
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
key!(PageUp) => SetNoteScroll(note_lo + 3), key!(PageUp) => SetNoteScroll(note_lo + 3),
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)), key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
key!(Right) => SetTimeScroll(time_start + 1), key!(Right) => SetTimeScroll(time_start + 1),
key!(Char('a')) => AppendNote, _ => return None
key!(Char('s')) => PutNote, },
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)), PhraseEditMode::Note => match from.event() {
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)), key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
key!(Up) => SetNoteCursor(note_point + 1),
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key!(PageUp) => SetNoteCursor(note_point + 3),
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_scale)),
key!(Right) => SetTimeCursor((time_point + time_scale) % length),
_ => return None _ => return None
}, },
} }
@ -69,7 +70,7 @@ impl Command<PhraseEditorModel> for PhraseCommand {
use PhraseCommand::*; use PhraseCommand::*;
Ok(match self { Ok(match self {
Show(phrase) => { Show(phrase) => {
state.phrase = phrase; state.show_phrase(phrase);
None None
}, },
ToggleDirection => { ToggleDirection => {

View file

@ -99,9 +99,13 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<Phra
key!(Char('t')) => Cmd::Length(Length::Begin), key!(Char('t')) => Cmd::Length(Length::Begin),
key!(Char('m')) => Cmd::Import(Browse::Begin), key!(Char('m')) => Cmd::Import(Browse::Begin),
key!(Char('x')) => Cmd::Export(Browse::Begin), key!(Char('x')) => Cmd::Export(Browse::Begin),
key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)),
key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()),
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
key!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
key!(Char(',')) => if index > 1 { key!(Char(',')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1)); state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index)) Cmd::Phrase(Pool::Swap(index - 1, index))
@ -120,16 +124,12 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<Phra
} else { } else {
return None return None
}, },
key!(Char('a')) => Cmd::Phrase(Pool::Add( key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
count, Phrase::new( String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) ))),
) key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
)), String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
key!(Char('i')) => Cmd::Phrase(Pool::Add( ))),
index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
)
)),
key!(Char('d')) => { key!(Char('d')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate(); let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);

View file

@ -67,15 +67,40 @@ impl Default for PhraseEditorModel {
} }
impl PhraseEditorModel { impl PhraseEditorModel {
/// Put note at current position
pub fn put_note (&mut self) { pub fn put_note (&mut self) {
todo!("put_note") if let Some(phrase) = &self.phrase {
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((127 - note) as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = Self::redraw(&phrase);
}
} }
/// Move time cursor forward by current note length
pub fn time_cursor_advance (&self) { pub fn time_cursor_advance (&self) {
let point = self.time_point.load(Ordering::Relaxed); let point = self.time_point.load(Ordering::Relaxed);
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length; let forward = |time|(time + self.note_len) % length;
self.time_point.store(forward(point), Ordering::Relaxed); self.time_point.store(forward(point), Ordering::Relaxed);
} }
/// 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>>>) {
if let Some(phrase) = phrase {
self.phrase = Some(phrase.clone());
self.time_clamp.store(phrase.read().unwrap().length, Ordering::Relaxed);
self.buffer = Self::redraw(&*phrase.read().unwrap());
} else {
self.phrase = None;
self.time_clamp.store(0, Ordering::Relaxed);
self.buffer = Default::default();
}
}
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]

View file

@ -1,25 +1,23 @@
use crate::*; use crate::*;
pub struct PhraseView<'a> { pub struct PhraseView<'a> {
pub(crate) focused: bool, focused: bool,
pub(crate) entered: bool, entered: bool,
pub(crate) phrase: &'a Option<Arc<RwLock<Phrase>>>, phrase: &'a Option<Arc<RwLock<Phrase>>>,
pub(crate) buffer: &'a BigBuffer, buffer: &'a BigBuffer,
pub(crate) note_len: usize, note_len: usize,
pub(crate) now: &'a Arc<Pulse>, now: &'a Arc<Pulse>,
size: &'a Measure<Tui>, size: &'a Measure<Tui>,
width: usize,
height: usize,
note_point: usize, note_point: usize,
note_range: (usize, usize), note_range: (usize, usize),
note_names: (&'a str, &'a str), note_names: (&'a str, &'a str),
pub(crate) time_start: usize, time_start: usize,
pub(crate) time_point: usize, time_point: usize,
pub(crate) time_clamp: usize, time_clamp: usize,
pub(crate) time_scale: usize, time_scale: usize,
} }
impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
@ -52,8 +50,6 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> {
now: &editor.now, now: &editor.now,
size: &editor.size, size: &editor.size,
width,
height,
note_point, note_point,
note_range: (note_lo, note_hi), note_range: (note_lo, note_hi),
@ -234,33 +230,7 @@ const NTH_OCTAVE: [&'static str; 11] = [
]; ];
impl PhraseEditorModel { impl PhraseEditorModel {
pub fn put (&mut self) { pub(crate) fn redraw (phrase: &Phrase) -> BigBuffer {
if let Some(phrase) = &self.phrase {
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((127 - note) as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = Self::redraw(&phrase);
}
}
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
if let Some(phrase) = phrase {
self.phrase = Some(phrase.clone());
self.time_clamp.store(phrase.read().unwrap().length, Ordering::Relaxed);
self.buffer = Self::redraw(&*phrase.read().unwrap());
} else {
self.phrase = None;
self.time_clamp.store(0, Ordering::Relaxed);
self.buffer = Default::default();
}
}
fn redraw (phrase: &Phrase) -> BigBuffer {
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
Self::fill_seq_fg(&mut buf, &phrase); Self::fill_seq_fg(&mut buf, &phrase);
@ -319,10 +289,10 @@ impl PhraseEditorModel {
break break
} }
if let Some(block) = half_block( if let Some(block) = half_block(
notes_on[y as usize * 2], notes_on[127 - (y as usize * 2)],
notes_on[y as usize * 2 + 1], notes_on[127 - (y as usize * 2 + 1)],
) { ) {
buf.get_mut(x, y).map(|cell|{ buf.get_mut(x, 64 - y).map(|cell|{
cell.set_char(block); cell.set_char(block);
cell.set_fg(Color::White); cell.set_fg(Color::White);
}); });