tek/crates/tek_tui/src/tui_view.rs
2024-11-21 00:25:54 +01:00

797 lines
29 KiB
Rust

use crate::*;
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T);
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
pub trait TransportViewState: ClockApi + PlayheadApi + Send + Sync {
fn transport_selected (&self) -> TransportFocus;
fn transport_focused (&self) -> bool;
fn transport_state (&self) -> Option<TransportState>;
fn bpm_value (&self) -> f64 {
self.bpm().get()
}
fn sync_value (&self) -> f64 {
self.sync().get()
}
fn format_beat (&self) -> String {
self.current().format_beat()
}
fn format_msu (&self) -> String {
self.current().usec.format_msu()
}
}
pub trait ArrangerViewState {
fn arranger_focused (&self) -> bool;
}
pub trait PhrasesViewState: Send + Sync {
fn phrases_focused (&self) -> bool;
fn entered (&self) -> bool;
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>>;
fn phrase (&self) -> usize;
fn phrase_mode (&self) -> &Option<PhrasesMode>;
}
pub trait PhraseViewState: Send + Sync {
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn phrase_focused (&self) -> bool;
fn phrase_editor_size (&self) -> &Measure<Tui>;
fn entered (&self) -> bool;
fn keys (&self) -> &Buffer;
fn buffer (&self) -> &BigBuffer;
fn note_len (&self) -> usize;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn now (&self) -> &Arc<Pulse>;
fn size (&self) -> &Measure<Tui>;
}
impl TransportViewState for TransportTui {
fn transport_selected (&self) -> TransportFocus {
self.state.focus
}
fn transport_focused (&self) -> bool {
true
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
}
impl TransportViewState for SequencerTui {
fn transport_selected (&self) -> TransportFocus {
self.transport.focus
}
fn transport_focused (&self) -> bool {
self.focused() == SequencerFocus::Transport
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
}
impl TransportViewState for ArrangerTui {
fn transport_selected (&self) -> TransportFocus {
self.transport.focus
}
fn transport_focused (&self) -> bool {
self.focused() == ArrangerFocus::Transport
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
}
impl ArrangerViewState for ArrangerTui {
fn arranger_focused (&self) -> bool {
self.focused() == ArrangerFocus::Arranger
}
}
impl PhrasesViewState for PhrasesModel {
fn phrases_focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn phrase_mode (&self) -> &Option<PhrasesMode> {
&self.mode
}
}
impl PhrasesViewState for SequencerTui {
fn phrases_focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn phrase_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
}
impl PhrasesViewState for ArrangerTui {
fn phrases_focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn phrase_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
}
impl PhraseViewState for PhraseEditorModel {
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
&self.phrase
}
fn phrase_focused (&self) -> bool {
self.focused
}
fn phrase_editor_size (&self) -> &Measure<Tui> {
todo!()
}
fn entered (&self) -> bool {
self.entered
}
fn keys (&self) -> &Buffer {
&self.keys
}
fn buffer (&self) -> &BigBuffer {
&self.buffer
}
fn note_len (&self) -> usize {
self.note_len
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self.note_axis
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self.time_axis
}
fn now (&self) -> &Arc<Pulse> {
&self.now
}
fn size (&self) -> &Measure<Tui> {
&self.size
}
}
impl PhraseViewState for SequencerTui {
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase_focused (&self) -> bool {
todo!()
}
fn phrase_editor_size (&self) -> &Measure<Tui> {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn keys (&self) -> &Buffer {
todo!()
}
fn buffer (&self) -> &BigBuffer {
todo!()
}
fn note_len (&self) -> usize {
todo!()
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
todo!()
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
todo!()
}
fn now (&self) -> &Arc<Pulse> {
todo!()
}
fn size (&self) -> &Measure<Tui> {
&self.size
}
}
impl PhraseViewState for ArrangerTui {
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase_focused (&self) -> bool {
todo!()
}
fn phrase_editor_size (&self) -> &Measure<Tui> {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn keys (&self) -> &Buffer {
todo!()
}
fn buffer (&self) -> &BigBuffer {
todo!()
}
fn note_len (&self) -> usize {
todo!()
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
todo!()
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
todo!()
}
fn now (&self) -> &Arc<Pulse> {
todo!()
}
fn size (&self) -> &Measure<Tui> {
&self.size
}
}
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
let mut widths = vec![];
let mut total = 0;
for track in tracks.iter() {
let width = track.width;
widths.push((width, total));
total += width;
}
widths.push((0, total));
widths
}
pub fn arranger_content_vertical (
view: &ArrangerTui,
factor: usize
) -> impl Widget<Engine = Tui> + use<'_> {
let timebase = view.timebase();
let current = view.current();
let tracks = view.tracks();
let scenes = view.scenes();
let cols = track_widths(tracks);
let rows = ArrangerScene::ppqs(scenes, factor);
let bg = view.color;
let clip_bg = TuiTheme::border_bg();
let sep_fg = TuiTheme::separator_fg(false);
let header_h = 3u16;//5u16;
let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track
let arrangement = Layers::new(move |add|{
let rows: &[(usize, usize)] = rows.as_ref();
let cols: &[(usize, usize)] = cols.as_ref();
let any_size = |_|Ok(Some([0,0]));
// column separators
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
let style = Some(Style::default().fg(sep_fg));
Ok(for x in cols.iter().map(|col|col.1) {
let x = scenes_w + to.area().x() + x as u16;
for y in to.area().y()..to.area().y2() { to.blit(&"", x, y, style); }
})
}))?;
// row separators
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
Ok(for y in rows.iter().map(|row|row.1) {
let y = to.area().y() + (y / PPQ) as u16 + 1;
if y >= to.buffer.area.height { break }
for x in to.area().x()..to.area().x2().saturating_sub(2) {
if x < to.buffer.area.x && y < to.buffer.area.y {
let cell = to.buffer.get_mut(x, y);
cell.modifier = Modifier::UNDERLINED;
cell.underline_color = sep_fg;
}
}
})
}))?;
// track titles
let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{
// name and width of track
let name = track.name().read().unwrap();
let max_w = w.saturating_sub(1).min(name.len()).max(2);
let name = format!("{}", &name[0..max_w]);
let name = TuiStyle::bold(name, true);
// beats elapsed
let elapsed = if let Some((_, Some(phrase))) = track.phrase().as_ref() {
let length = phrase.read().unwrap().length;
let elapsed = track.pulses_since_start().unwrap();
let elapsed = timebase.format_beats_1_short(
(elapsed as usize % length) as f64
);
format!("▎+{elapsed:>}")
} else {
String::from("")
};
// beats until switchover
let until_next = track.next_phrase().as_ref().map(|(t, _)|{
let target = t.pulse.get();
let current = current.pulse.get();
if target > current {
let remaining = target - current;
format!("▎-{:>}", timebase.format_beats_0_short(remaining))
} else {
String::new()
}
}).unwrap_or(String::from(""));
// name of active MIDI input
let input = format!("▎>{}", track.midi_ins().get(0)
.map(|port|port.short_name())
.transpose()?
.unwrap_or("(none)".into()));
// name of active MIDI output
let output = format!("▎<{}", track.midi_outs().get(0)
.map(|port|port.short_name())
.transpose()?
.unwrap_or("(none)".into()));
col!(name, /*input, output,*/ until_next, elapsed)
.min_xy(w as u16, header_h)
.bg(track.color().rgb)
.push_x(scenes_w)
});
// tracks and scenes
let content = col!(
// scenes:
(scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => {
let height = 1.max((pulses / PPQ) as u16);
let playing = scene.is_playing(tracks);
Stack::right(move |add| {
// scene title:
add(&row!(
if playing { "" } else { " " },
TuiStyle::bold(scene.name.read().unwrap().as_str(), true),
).fixed_xy(scenes_w, height).bg(scene.color.rgb))?;
// clip per track:
Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() {
add(&Layers::new(move |add|{
let mut bg = clip_bg;
match (tracks.get(track), scene.clips.get(track)) {
(Some(track), Some(Some(phrase))) => {
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
let name = format!("{}", name);
let max_w = name.len().min((w as usize).saturating_sub(2));
let color = phrase.read().unwrap().color;
add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?;
bg = color.dark.rgb;
if let Some((_, Some(ref playing))) = track.phrase() {
if *playing.read().unwrap() == *phrase.read().unwrap() {
bg = color.light.rgb
}
};
},
_ => {}
};
add(&Background(bg))
}).fixed_xy(w as u16, height))?;
})
}).fixed_y(height)
}
).fixed_y((view.size.h() as u16).saturating_sub(header_h));
// full grid with header and footer
add(&col!(header, content))?;
// cursor
add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{
let area = to.area();
let focused = view.arranger_focused();
let selected = view.selected;
let get_track_area = |t: usize| [
scenes_w + area.x() + cols[t].1 as u16, area.y(),
cols[t].0 as u16, area.h(),
];
let get_scene_area = |s: usize| [
area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16,
area.w(), (rows[s].0 / PPQ) as u16
];
let get_clip_area = |t: usize, s: usize| [
scenes_w + area.x() + cols[t].1 as u16,
header_h + area.y() + (rows[s].1/PPQ) as u16,
cols[t].0 as u16,
(rows[s].0 / PPQ) as u16
];
let mut track_area: Option<[u16;4]> = None;
let mut scene_area: Option<[u16;4]> = None;
let mut clip_area: Option<[u16;4]> = None;
let area = match selected {
ArrangerSelection::Mix => area,
ArrangerSelection::Track(t) => {
track_area = Some(get_track_area(t));
area
},
ArrangerSelection::Scene(s) => {
scene_area = Some(get_scene_area(s));
area
},
ArrangerSelection::Clip(t, s) => {
track_area = Some(get_track_area(t));
scene_area = Some(get_scene_area(s));
clip_area = Some(get_clip_area(t, s));
area
},
};
let bg = TuiTheme::border_bg();
if let Some([x, y, width, height]) = track_area {
to.fill_fg([x, y, 1, height], bg);
to.fill_fg([x + width, y, 1, height], bg);
}
if let Some([_, y, _, height]) = scene_area {
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
}
Ok(if focused {
to.render_in(if let Some(clip_area) = clip_area { clip_area }
else if let Some(track_area) = track_area { track_area.clip_h(header_h) }
else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) }
else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)?
})
}))
}).bg(bg.rgb);
let color = TuiTheme::title_fg(view.arranger_focused());
let size = format!("{}x{}", view.size.w(), view.size.h());
let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy();
lay!(arrangement, lower_right)
}
pub fn arranger_content_horizontal (
view: &ArrangerTui,
) -> impl Widget<Engine = Tui> + use<'_> {
let focused = view.arranger_focused();
let _tracks = view.tracks();
lay!(
focused.then_some(Background(TuiTheme::border_bg())),
row!(
// name
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks, selected) = self;
//let yellow = Some(Style::default().yellow().bold().not_dim());
//let white = Some(Style::default().white().bold().not_dim());
//let area = to.area();
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
//let offset = 0; // track scroll offset
//for y in 0..area.h() {
//if y == 0 {
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2 + offset;
//if let Some(track) = tracks.get(index) {
//let selected = selected.track() == Some(index);
//let style = if selected { yellow } else { white };
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
//}
//}
//}
//Ok(Some(area))
}),
// monitor
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().green().bold());
//let off = Some(DIM);
//area.x += 1;
//for y in 0..area.h() {
//if y == 0 {
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//let style = if track.monitoring { on } else { off };
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// record
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().red().bold());
//let off = Some(Style::default().dim());
//area.x += 1;
//for y in 0..area.h() {
//if y == 0 {
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//let style = if track.recording { on } else { off };
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// overdub
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let on = Some(Style::default().not_dim().yellow().bold());
//let off = Some(Style::default().dim());
//area.x = area.x + 1;
//for y in 0..area.h() {
//if y == 0 {
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(track) = tracks.get(index) {
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
//on
//} else {
//off
//})?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// erase
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let off = Some(Style::default().dim());
//area.x = area.x + 1;
//for y in 0..area.h() {
//if y == 0 {
////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(_) = tracks.get(index) {
//to.blit(&" DEL ", area.x(), area.y() + y, off)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 4;
//Ok(Some(area))
}),
// gain
CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{
todo!()
//let Self(tracks) = self;
//let mut area = to.area();
//let off = Some(Style::default().dim());
//area.x = area.x() + 1;
//for y in 0..area.h() {
//if y == 0 {
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
//} else if y % 2 == 0 {
//let index = (y as usize - 2) / 2;
//if let Some(_) = tracks.get(index) {
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
//} else {
//area.height = y;
//break
//}
//}
//}
//area.width = 7;
//Ok(Some(area))
}),
// scenes
CustomWidget::new(|_|{todo!()}, |to: &mut TuiOutput|{
let [x, y, _, height] = to.area();
let mut x2 = 0;
Ok(for (scene_index, scene) in view.scenes().iter().enumerate() {
let active_scene = view.selected.scene() == Some(scene_index);
let sep = Some(if active_scene {
Style::default().yellow().not_dim()
} else {
Style::default().dim()
});
for y in y+1..y+height {
to.blit(&"", x + x2, y, sep);
}
let name = scene.name.read().unwrap();
let mut x3 = name.len() as u16;
to.blit(&*name, x + x2, y, sep);
for (i, clip) in scene.clips.iter().enumerate() {
let active_track = view.selected.track() == Some(i);
if let Some(clip) = clip {
let y2 = y + 2 + i as u16 * 2;
let label = format!("{}", clip.read().unwrap().name);
to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
Style::default().not_dim().yellow().bold()
} else {
Style::default().not_dim()
}));
x3 = x3.max(label.len() as u16)
}
}
x2 = x2 + x3 + 1;
})
}),
)
)
}
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
];
pub(crate) fn keys_vert () -> Buffer {
let area = [0, 0, 5, 64];
let mut buffer = Buffer::empty(Rect {
x: area.x(), y: area.y(), width: area.w(), height: area.h()
});
buffer_update(&mut buffer, area, &|cell, x, y| {
let y = 63 - y;
match x {
0 => {
cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_char('▀');
cell.set_fg(Color::White);
cell.set_bg(Color::White);
},
2 => if y % 6 == 0 {
cell.set_char('C');
},
3 => if y % 6 == 0 {
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
},
_ => {}
}
});
buffer
}
const NTH_OCTAVE: [&'static str; 11] = [
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
];
impl PhraseEditorModel {
pub fn put (&mut self) {
if let (Some(phrase), Some(time), Some(note)) = (
&self.phrase,
self.time_axis.read().unwrap().point,
self.note_axis.read().unwrap().point,
) {
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_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
self.buffer = Self::redraw(&*phrase.read().unwrap());
} else {
self.phrase = None;
self.time_axis.write().unwrap().clamp = Some(0);
self.buffer = Default::default();
}
}
fn redraw (phrase: &Phrase) -> BigBuffer {
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
Self::fill_seq_fg(&mut buf, &phrase);
buf
}
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
for x in 0..buf.width {
// Only fill as far as phrase length
if x as usize >= length { break }
// Fill each row with background characters
for y in 0 .. buf.height {
buf.get_mut(x, y).map(|cell|{
cell.set_char(if ppq == 0 {
'·'
} else if x % (4 * ppq) == 0 {
'│'
} else if x % ppq == 0 {
'╎'
} else {
'·'
});
cell.set_fg(Color::Rgb(48, 64, 56));
cell.modifier = Modifier::DIM;
});
}
}
}
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
let mut notes_on = [false;128];
for x in 0..buf.width {
if x as usize >= phrase.length {
break
}
if let Some(notes) = phrase.notes.get(x as usize) {
if phrase.percussive {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
_ => {}
}
}
} else {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } =>
notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
for y in 0..buf.height {
if y >= 64 {
break
}
if let Some(block) = half_block(
notes_on[y as usize * 2],
notes_on[y as usize * 2 + 1],
) {
buf.get_mut(x, y).map(|cell|{
cell.set_char(block);
cell.set_fg(Color::White);
});
}
}
if phrase.percussive {
notes_on.fill(false);
}
}
}
}
}