mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
2423 lines
83 KiB
Rust
2423 lines
83 KiB
Rust
use crate::*;
|
||
|
||
const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN);
|
||
|
||
tui_style!(GRAY_DIM =
|
||
Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty());
|
||
tui_style!(GRAY_NOT_DIM_BOLD =
|
||
Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM);
|
||
tui_style!(NOT_DIM_BOLD =
|
||
None, None, None, Modifier::BOLD, Modifier::DIM);
|
||
tui_style!(NOT_DIM_GREEN =
|
||
Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM);
|
||
tui_style!(NOT_DIM =
|
||
None, None, None, Modifier::empty(), Modifier::DIM);
|
||
tui_style!(WHITE_NOT_DIM_BOLD =
|
||
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
||
tui_style!(STYLE_LABEL =
|
||
Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD);
|
||
tui_style!(STYLE_VALUE =
|
||
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// Represents the tracks and scenes of the composition.
|
||
pub struct Arranger<E: Engine> {
|
||
/// Name of arranger
|
||
pub name: Arc<RwLock<String>>,
|
||
/// Collection of tracks.
|
||
pub tracks: Vec<Sequencer<E>>,
|
||
/// Collection of scenes.
|
||
pub scenes: Vec<Scene>,
|
||
/// Currently selected element.
|
||
pub selected: ArrangerFocus,
|
||
/// Display mode of arranger
|
||
pub mode: ArrangerViewMode,
|
||
/// Slot for modal dialog displayed on top of app.
|
||
pub modal: Option<Box<dyn ExitableComponent<E>>>,
|
||
/// Whether the arranger is currently focused
|
||
pub focused: bool
|
||
}
|
||
|
||
impl<E: Engine> Arranger<E> {
|
||
pub fn new (name: &str) -> Self {
|
||
Self {
|
||
name: Arc::new(RwLock::new(name.into())),
|
||
mode: ArrangerViewMode::VerticalCompact2,
|
||
selected: ArrangerFocus::Clip(0, 0),
|
||
scenes: vec![],
|
||
tracks: vec![],
|
||
modal: None,
|
||
focused: false
|
||
}
|
||
}
|
||
pub fn activate (&mut self) {
|
||
match self.selected {
|
||
ArrangerFocus::Scene(s) => {
|
||
for (track_index, track) in self.tracks.iter_mut().enumerate() {
|
||
track.sequence = self.scenes[s].clips[track_index];
|
||
track.reset = true;
|
||
}
|
||
},
|
||
ArrangerFocus::Clip(t, s) => {
|
||
self.tracks[t].sequence = self.scenes[s].clips[t];
|
||
self.tracks[t].reset = true;
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
pub fn sequencer (&self) -> Option<&Sequencer<E>> {
|
||
self.selected.track()
|
||
.map(|track|self.tracks.get(track))
|
||
.flatten()
|
||
}
|
||
pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
||
self.selected.track()
|
||
.map(|track|self.tracks.get_mut(track))
|
||
.flatten()
|
||
}
|
||
pub fn show_phrase (&mut self) -> Usually<()> {
|
||
//unimplemented!()
|
||
//let phrase = self.phrase();
|
||
//self.sequencer.show(phrase)
|
||
Ok(())
|
||
}
|
||
pub fn is_first_row (&self) -> bool {
|
||
let selected = self.selected;
|
||
selected.is_mix() || selected.is_track() || match selected {
|
||
ArrangerFocus::Clip(_, s) =>
|
||
s == 0,
|
||
_ => false
|
||
}
|
||
}
|
||
pub fn is_last_row (&self) -> bool {
|
||
let selected = self.selected;
|
||
match selected {
|
||
ArrangerFocus::Scene(s) =>
|
||
s == self.scenes.len() - 1,
|
||
ArrangerFocus::Clip(_, s) =>
|
||
s == self.scenes.len() - 1,
|
||
_ => false
|
||
}
|
||
}
|
||
pub fn track (&self) -> Option<&Sequencer<E>> {
|
||
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
||
}
|
||
pub fn track_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
||
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
||
}
|
||
pub fn track_next (&mut self) {
|
||
self.selected.track_next(self.tracks.len() - 1)
|
||
}
|
||
pub fn track_prev (&mut self) {
|
||
self.selected.track_prev()
|
||
}
|
||
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Sequencer<E>> {
|
||
self.tracks.push(name.map_or_else(
|
||
|| Sequencer::new(&self.track_default_name()),
|
||
|name| Sequencer::new(name),
|
||
));
|
||
let index = self.tracks.len() - 1;
|
||
Ok(&mut self.tracks[index])
|
||
}
|
||
pub fn track_del (&mut self) {
|
||
unimplemented!("Arranger::track_del");
|
||
}
|
||
pub fn track_default_name (&self) -> String {
|
||
format!("Track {}", self.tracks.len() + 1)
|
||
}
|
||
pub fn scene (&self) -> Option<&Scene> {
|
||
self.selected.scene().map(|s|self.scenes.get(s)).flatten()
|
||
}
|
||
pub fn scene_mut (&mut self) -> Option<&mut Scene> {
|
||
self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten()
|
||
}
|
||
pub fn scene_next (&mut self) {
|
||
self.selected.scene_next(self.scenes.len() - 1)
|
||
}
|
||
pub fn scene_prev (&mut self) {
|
||
self.selected.scene_prev()
|
||
}
|
||
pub fn scene_add (&mut self, name: Option<&str>) -> Usually<&mut Scene> {
|
||
let clips = vec![None;self.tracks.len()];
|
||
self.scenes.push(match name {
|
||
Some(name) => Scene::new(name, clips),
|
||
None => Scene::new(&self.scene_default_name(), clips),
|
||
});
|
||
let index = self.scenes.len() - 1;
|
||
Ok(&mut self.scenes[index])
|
||
}
|
||
pub fn scene_del (&mut self) {
|
||
unimplemented!("Arranger::scene_del");
|
||
}
|
||
pub fn scene_default_name (&self) -> String {
|
||
format!("Scene {}", self.scenes.len() + 1)
|
||
}
|
||
pub fn phrase (&self) -> Option<&Arc<RwLock<Phrase>>> {
|
||
let track_id = self.selected.track()?;
|
||
self.tracks.get(track_id)?.phrases.get((*self.scene()?.clips.get(track_id)?)?)
|
||
}
|
||
pub fn phrase_next (&mut self) {
|
||
let track_index = self.selected.track();
|
||
let scene_index = self.selected.scene();
|
||
track_index
|
||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||
.and_then(|(track_index, track)|{
|
||
let phrases = track.phrases.len();
|
||
scene_index
|
||
.and_then(|index|self.scenes.get_mut(index))
|
||
.and_then(|scene|{
|
||
if let Some(phrase_index) = scene.clips[track_index] {
|
||
if phrase_index >= phrases - 1 {
|
||
scene.clips[track_index] = None;
|
||
} else {
|
||
scene.clips[track_index] = Some(phrase_index + 1);
|
||
}
|
||
} else if phrases > 0 {
|
||
scene.clips[track_index] = Some(0);
|
||
}
|
||
Some(())
|
||
})
|
||
});
|
||
}
|
||
pub fn phrase_prev (&mut self) {
|
||
let track_index = self.selected.track();
|
||
let scene_index = self.selected.scene();
|
||
track_index
|
||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||
.and_then(|(track_index, track)|{
|
||
let phrases = track.phrases.len();
|
||
scene_index
|
||
.and_then(|index|self.scenes.get_mut(index))
|
||
.and_then(|scene|{
|
||
if let Some(phrase_index) = scene.clips[track_index] {
|
||
scene.clips[track_index] = if phrase_index == 0 {
|
||
None
|
||
} else {
|
||
Some(phrase_index - 1)
|
||
};
|
||
} else if phrases > 0 {
|
||
scene.clips[track_index] = Some(phrases - 1);
|
||
}
|
||
Some(())
|
||
})
|
||
});
|
||
}
|
||
}
|
||
|
||
impl Arranger<Tui> {
|
||
pub fn rename_selected (&mut self) {
|
||
self.modal = Some(Box::new(ArrangerRenameModal::new(
|
||
self.selected,
|
||
&match self.selected {
|
||
ArrangerFocus::Mix => self.name.clone(),
|
||
ArrangerFocus::Track(t) => self.tracks[t].name.clone(),
|
||
ArrangerFocus::Scene(s) => self.scenes[s].name.clone(),
|
||
ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(),
|
||
}
|
||
)));
|
||
}
|
||
}
|
||
|
||
impl Handle<Tui> for Arranger<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
if let Some(modal) = self.modal.as_mut() {
|
||
let result = modal.handle(from)?;
|
||
if modal.exited() {
|
||
self.modal = None;
|
||
}
|
||
return Ok(result)
|
||
}
|
||
match from.event() {
|
||
// mode_switch: switch the display mode
|
||
key!(KeyCode::Char('`')) => {
|
||
self.mode.to_next()
|
||
},
|
||
// cursor_up: move cursor up
|
||
key!(KeyCode::Up) => {
|
||
match self.mode {
|
||
ArrangerViewMode::Horizontal => self.track_prev(),
|
||
_ => self.scene_prev(),
|
||
};
|
||
self.show_phrase()?;
|
||
},
|
||
// cursor_down
|
||
key!(KeyCode::Down) => {
|
||
match self.mode {
|
||
ArrangerViewMode::Horizontal => self.track_next(),
|
||
_ => self.scene_next(),
|
||
};
|
||
self.show_phrase()?;
|
||
},
|
||
// cursor left
|
||
key!(KeyCode::Left) => {
|
||
match self.mode {
|
||
ArrangerViewMode::Horizontal => self.scene_prev(),
|
||
_ => self.track_prev(),
|
||
};
|
||
self.show_phrase()?;
|
||
},
|
||
// cursor right
|
||
key!(KeyCode::Right) => {
|
||
match self.mode {
|
||
ArrangerViewMode::Horizontal => self.scene_next(),
|
||
_ => self.track_next(),
|
||
};
|
||
self.show_phrase()?;
|
||
},
|
||
// increment: use next clip here
|
||
key!(KeyCode::Char('.')) => {
|
||
self.phrase_next();
|
||
},
|
||
// decrement: use previous next clip here
|
||
key!(KeyCode::Char(',')) => {
|
||
self.phrase_prev();
|
||
},
|
||
// decrement: use previous clip here
|
||
key!(KeyCode::Enter) => {
|
||
self.activate();
|
||
},
|
||
// scene_add: add a new scene
|
||
key!(Ctrl-KeyCode::Char('a')) => {
|
||
self.scene_add(None)?;
|
||
},
|
||
// track_add: add a new scene
|
||
key!(Ctrl-KeyCode::Char('t')) => {
|
||
self.track_add(None)?;
|
||
},
|
||
// rename: add a new scene
|
||
key!(KeyCode::Char('n')) => {
|
||
self.rename_selected();
|
||
},
|
||
// length: add a new scene
|
||
key!(KeyCode::Char('l')) => {
|
||
todo!();
|
||
},
|
||
// color: set color of item at cursor
|
||
key!(KeyCode::Char('c')) => {
|
||
todo!();
|
||
},
|
||
_ => return Ok(None)
|
||
}
|
||
Ok(Some(true))
|
||
}
|
||
}
|
||
|
||
impl Content for Arranger<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
Layers::new(move |add|{
|
||
//Lozenge(Style::default().fg(Nord::BG2))
|
||
//.draw(&mut to.alter_area(|[x, y, w, h]|[
|
||
//x.saturating_sub(1),
|
||
//y.saturating_sub(1),
|
||
//w + 2,
|
||
//h + 2,
|
||
//]))
|
||
match self.mode {
|
||
ArrangerViewMode::Horizontal => add(&ArrangerViewHorizontal(&self)),
|
||
ArrangerViewMode::VerticalCompact1 => add(&ArrangerViewVertical(
|
||
&self,
|
||
track_clip_name_lengths(self.tracks.as_slice()).as_slice(),
|
||
(0..=self.scenes.len()).map(|i|(96, 96*i)).collect::<Vec<_>>().as_slice(),
|
||
)),
|
||
ArrangerViewMode::VerticalCompact2 => add(&ArrangerViewVertical(
|
||
&self,
|
||
track_clip_name_lengths(self.tracks.as_slice()).as_slice(),
|
||
(0..=self.scenes.len()).map(|i|(192, 192*i)).collect::<Vec<_>>().as_slice()
|
||
)),
|
||
ArrangerViewMode::VerticalExpanded => add(&ArrangerViewVertical(
|
||
&self,
|
||
track_clip_name_lengths(self.tracks.as_slice()).as_slice(),
|
||
scene_ppqs(self.tracks.as_slice(), self.scenes.as_slice()).as_slice(),
|
||
)),
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
pub fn scene_ppqs <E: Engine> (
|
||
tracks: &[Sequencer<E>],
|
||
scenes: &[Scene]
|
||
) -> Vec<(usize, usize)> {
|
||
let mut total = 0;
|
||
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
|
||
let pulses = scene.pulses(tracks).max(96);
|
||
total = total + pulses;
|
||
(pulses, total - pulses)
|
||
}).collect();
|
||
scenes.push((0, total));
|
||
scenes
|
||
}
|
||
|
||
pub fn track_clip_name_lengths <E: Engine> (tracks: &[Sequencer<E>]) -> Vec<(usize, usize)> {
|
||
let mut total = 0;
|
||
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
|
||
let len = 4 + track.phrases
|
||
.iter()
|
||
.fold(track.name.read().unwrap().len(), |len, phrase|{
|
||
len.max(phrase.read().unwrap().name.read().unwrap().len())
|
||
});
|
||
total = total + len;
|
||
(len, total - len)
|
||
}).collect();
|
||
lengths.push((0, total));
|
||
lengths
|
||
}
|
||
|
||
impl Focusable<Tui> for Arranger<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
#[derive(PartialEq, Clone, Copy)]
|
||
/// Represents the current user selection in the arranger
|
||
pub enum ArrangerFocus {
|
||
/** The whole mix is selected */
|
||
Mix,
|
||
/// A track is selected.
|
||
Track(usize),
|
||
/// A scene is selected.
|
||
Scene(usize),
|
||
/// A clip (track × scene) is selected.
|
||
Clip(usize, usize),
|
||
}
|
||
|
||
/// Focus identification methods
|
||
impl ArrangerFocus {
|
||
pub fn is_mix (&self) -> bool {
|
||
match self { Self::Mix => true, _ => false }
|
||
}
|
||
pub fn is_track (&self) -> bool {
|
||
match self { Self::Track(_) => true, _ => false }
|
||
}
|
||
pub fn is_scene (&self) -> bool {
|
||
match self { Self::Scene(_) => true, _ => false }
|
||
}
|
||
pub fn is_clip (&self) -> bool {
|
||
match self { Self::Clip(_, _) => true, _ => false }
|
||
}
|
||
pub fn track (&self) -> Option<usize> {
|
||
match self {
|
||
Self::Clip(t, _) => Some(*t),
|
||
Self::Track(t) => Some(*t),
|
||
_ => None
|
||
}
|
||
}
|
||
pub fn track_next (&mut self, last_track: usize) {
|
||
*self = match self {
|
||
Self::Mix => Self::Track(0),
|
||
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
|
||
Self::Scene(s) => Self::Clip(0, *s),
|
||
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
|
||
}
|
||
}
|
||
pub fn track_prev (&mut self) {
|
||
*self = match self {
|
||
Self::Mix => Self::Mix,
|
||
Self::Scene(s) => Self::Scene(*s),
|
||
Self::Track(t) => if *t == 0 {
|
||
Self::Mix
|
||
} else {
|
||
Self::Track(*t - 1)
|
||
},
|
||
Self::Clip(t, s) => if *t == 0 {
|
||
Self::Scene(*s)
|
||
} else {
|
||
Self::Clip(t.saturating_sub(1), *s)
|
||
}
|
||
}
|
||
}
|
||
pub fn scene (&self) -> Option<usize> {
|
||
match self {
|
||
Self::Clip(_, s) => Some(*s),
|
||
Self::Scene(s) => Some(*s),
|
||
_ => None
|
||
}
|
||
}
|
||
pub fn scene_next (&mut self, last_scene: usize) {
|
||
*self = match self {
|
||
Self::Mix => Self::Scene(0),
|
||
Self::Track(t) => Self::Clip(*t, 0),
|
||
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
|
||
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
|
||
}
|
||
}
|
||
pub fn scene_prev (&mut self) {
|
||
*self = match self {
|
||
Self::Mix => Self::Mix,
|
||
Self::Track(t) => Self::Track(*t),
|
||
Self::Scene(s) => if *s == 0 {
|
||
Self::Mix
|
||
} else {
|
||
Self::Scene(*s - 1)
|
||
},
|
||
Self::Clip(t, s) => if *s == 0 {
|
||
Self::Track(*t)
|
||
} else {
|
||
Self::Clip(*t, s.saturating_sub(1))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// Display mode of arranger
|
||
#[derive(PartialEq)]
|
||
pub enum ArrangerViewMode { VerticalExpanded, VerticalCompact1, VerticalCompact2, Horizontal }
|
||
|
||
/// Arranger display mode can be cycled
|
||
impl ArrangerViewMode {
|
||
/// Cycle arranger display mode
|
||
pub fn to_next (&mut self) {
|
||
*self = match self {
|
||
Self::VerticalExpanded => Self::VerticalCompact1,
|
||
Self::VerticalCompact1 => Self::VerticalCompact2,
|
||
Self::VerticalCompact2 => Self::Horizontal,
|
||
Self::Horizontal => Self::VerticalExpanded,
|
||
}
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct ArrangerViewVertical<'a, 'b, E: Engine>(
|
||
&'a Arranger<E>, &'b [(usize, usize)], &'b [(usize, usize)]
|
||
);
|
||
|
||
impl<'a, 'b> Content for ArrangerViewVertical<'a, 'b, Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
let Self(state, cols, rows) = self;
|
||
let tracks: &[Sequencer<Tui>] = state.tracks.as_ref();
|
||
let scenes = state.scenes.as_ref();
|
||
let offset = 4 + scene_name_max_len(scenes) as u16;
|
||
Layers::new(move |add|{
|
||
add(&ColumnSeparators(offset, cols))?;
|
||
add(&RowSeparators(rows))?;
|
||
add(&CursorFocus(state.focused, state.selected, offset, cols, rows))?;
|
||
add(&Split::down(|add|{
|
||
add(&Push::X(offset, Split::right(move |add|{
|
||
for (track, (w, _)) in tracks.iter().zip(*cols) {
|
||
add(&Min::XY(*w as u16, 2, Layers::new(|add|{
|
||
add(&Background(COLOR_BG1))?;
|
||
add(&track.name.read().unwrap().as_str())
|
||
})))?;
|
||
}
|
||
Ok(())
|
||
})))?;
|
||
add(&Split::down(move |add| {
|
||
for (scene, (pulses, _)) in scenes.iter().zip(*rows) {
|
||
let height = 1.max((pulses / 96) as u16);
|
||
let playing = scene.is_playing(tracks);
|
||
add(&Fixed::Y(height, Split::right(move |add| {
|
||
add(&Fixed::XY(offset.saturating_sub(1), height, Split::right(|add|{
|
||
add(&if playing { "▶ " } else { " " })?;
|
||
add(&scene.name.read().unwrap().as_str())
|
||
})))?;
|
||
for (track, (w, _x)) in cols.iter().enumerate() {
|
||
add(&Fixed::XY(*w as u16, height, Layers::new(move |add|{
|
||
let mut color = COLOR_BG0;
|
||
if let (Some(track), Some(Some(clip))) = (
|
||
tracks.get(track),
|
||
scene.clips.get(track),
|
||
) {
|
||
if let Some(phrase) = track.phrases.get(*clip) {
|
||
add(&Push::X(1, format!(
|
||
"{clip:02} {}",
|
||
phrase.read().unwrap().name.read().unwrap()
|
||
).as_str()))?;
|
||
color = if track.sequence == Some(*clip) {
|
||
Nord::PLAYING
|
||
} else {
|
||
COLOR_BG1
|
||
};
|
||
}
|
||
}
|
||
add(&Background(color))
|
||
})))?;
|
||
}
|
||
Ok(())
|
||
})))?;
|
||
}
|
||
Ok(())
|
||
}))
|
||
}))
|
||
})
|
||
}
|
||
}
|
||
|
||
pub fn scene_name_max_len (scenes: &[Scene]) -> usize {
|
||
scenes.iter()
|
||
.map(|s|s.name.read().unwrap().len())
|
||
.fold(0, usize::max)
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct ColumnSeparators<'a>(u16, &'a [(usize, usize)]);
|
||
|
||
impl<'a> Widget for ColumnSeparators<'a> {
|
||
type Engine = Tui;
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
let Self(offset, cols) = self;
|
||
let style = Some(Style::default().fg(Nord::SEPARATOR));
|
||
for (_, x) in cols.iter() {
|
||
let x = offset + area.x() + *x as u16 - 1;
|
||
for y in area.y()..area.y2() {
|
||
to.blit(&"▎", x, y, style);
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct RowSeparators<'a>(&'a [(usize, usize)]);
|
||
|
||
impl<'a> Widget for RowSeparators<'a> {
|
||
type Engine = Tui;
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
let Self(rows) = self;
|
||
for (_, y) in rows.iter() {
|
||
let y = area.y() + (*y / 96) as u16 + 1;
|
||
if y >= to.buffer.area.height {
|
||
break
|
||
}
|
||
for x in area.x()..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 = Nord::SEPARATOR;
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct CursorFocus<'a>(
|
||
bool, ArrangerFocus, u16, &'a [(usize, usize)], &'a [(usize, usize)]
|
||
);
|
||
|
||
impl<'a> Widget for CursorFocus<'a> {
|
||
type Engine = Tui;
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
let Self(focused, selected, offset, cols, rows) = *self;
|
||
let get_track_area = |t: usize| [
|
||
offset + area.x() + cols[t].1 as u16 - 1,
|
||
area.y(),
|
||
cols[t].0 as u16,
|
||
area.h()
|
||
];
|
||
let get_scene_area = |s: usize| [
|
||
area.x(),
|
||
2 + area.y() + (rows[s].1 / 96) as u16,
|
||
area.w(),
|
||
(rows[s].0 / 96) as u16
|
||
];
|
||
let get_clip_area = |t: usize, s: usize| [
|
||
offset + area.x() + cols[t].1 as u16 - 1,
|
||
2 + area.y() + (rows[s].1 / 96) as u16,
|
||
cols[t].0 as u16,
|
||
(rows[s].0 / 96) 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 {
|
||
ArrangerFocus::Mix => {
|
||
if focused {
|
||
to.fill_bg(area, COLOR_BG0);
|
||
}
|
||
area
|
||
},
|
||
ArrangerFocus::Track(t) => {
|
||
track_area = Some(get_track_area(t));
|
||
area
|
||
},
|
||
ArrangerFocus::Scene(s) => {
|
||
scene_area = Some(get_scene_area(s));
|
||
area
|
||
},
|
||
ArrangerFocus::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
|
||
},
|
||
};
|
||
if let Some([x, y, width, height]) = track_area {
|
||
to.fill_fg([x, y, 1, height], COLOR_BG5);
|
||
to.fill_fg([x + width, y, 1, height], COLOR_BG5);
|
||
}
|
||
if let Some([_, y, _, height]) = scene_area {
|
||
to.fill_ul([area.x(), y - 1, area.w(), 1], COLOR_BG5);
|
||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], COLOR_BG5);
|
||
}
|
||
if focused {
|
||
if let Some(clip_area) = clip_area {
|
||
to.fill_bg(clip_area, COLOR_BG0);
|
||
} else if let Some(track_area) = track_area {
|
||
to.fill_bg(track_area, COLOR_BG0);
|
||
} else if let Some(scene_area) = scene_area {
|
||
to.fill_bg(scene_area, COLOR_BG0);
|
||
}
|
||
}
|
||
//Ok(Some(area))
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct ArrangerViewHorizontal<'a, E: Engine>(
|
||
&'a Arranger<E>
|
||
);
|
||
|
||
impl<'a> Content for ArrangerViewHorizontal<'a, Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
let Arranger { tracks, focused, selected, scenes, .. } = self.0;
|
||
let tracks = tracks.as_slice();
|
||
Layers::new(|add|{
|
||
add(&focused.then_some(Background(COLOR_BG0)))?;
|
||
add(&Split::right(|add|{
|
||
add(&TrackNameColumn(tracks, *selected))?;
|
||
add(&TrackMonitorColumn(tracks))?;
|
||
add(&TrackRecordColumn(tracks))?;
|
||
add(&TrackOverdubColumn(tracks))?;
|
||
add(&TrackEraseColumn(tracks))?;
|
||
add(&TrackGainColumn(tracks))?;
|
||
add(&TrackScenesColumn(tracks, scenes.as_slice(), *selected))?;
|
||
Ok(())
|
||
}))
|
||
})
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackNameColumn<'a>(&'a [Sequencer<Tui>], ArrangerFocus);
|
||
|
||
impl<'a> Widget for TrackNameColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, _to: &mut TuiOutput) -> Usually<()> {
|
||
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))
|
||
}
|
||
}
|
||
|
||
pub fn track_name_max_len <E: Engine> (tracks: &[Sequencer<E>]) -> usize {
|
||
tracks.iter()
|
||
.map(|s|s.name.read().unwrap().len())
|
||
.fold(0, usize::max)
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackMonitorColumn<'a>(&'a [Sequencer<Tui>]);
|
||
|
||
impl<'a> Widget for TrackMonitorColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, _to: &mut TuiOutput) -> Usually<()> {
|
||
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))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackRecordColumn<'a>(&'a [Sequencer<Tui>]);
|
||
|
||
impl<'a> Widget for TrackRecordColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, _to: &mut TuiOutput) -> Usually<()> {
|
||
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))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackOverdubColumn<'a>(&'a [Sequencer<Tui>]);
|
||
|
||
impl<'a> Widget for TrackOverdubColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, _to: &mut TuiOutput) -> Usually<()> {
|
||
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))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackEraseColumn<'a>(&'a [Sequencer<Tui>]);
|
||
|
||
impl<'a> Widget for TrackEraseColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, _to: &mut TuiOutput) -> Usually<()> {
|
||
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))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackGainColumn<'a>(&'a [Sequencer<Tui>]);
|
||
|
||
impl<'a> Widget for TrackGainColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, _to: &mut TuiOutput) -> Usually<()> {
|
||
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))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct TrackScenesColumn<'a>(&'a [Sequencer<Tui>], &'a [Scene], ArrangerFocus);
|
||
|
||
impl<'a> Widget for TrackScenesColumn<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let Self(tracks, scenes, selected) = self;
|
||
let area = to.area();
|
||
let mut x2 = 0;
|
||
let [x, y, _, height] = area;
|
||
for (scene_index, scene) in scenes.iter().enumerate() {
|
||
let active_scene = 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 = selected.track() == Some(i);
|
||
if let Some(clip) = clip {
|
||
let y2 = y + 2 + i as u16 * 2;
|
||
let label = match tracks[i].phrases.get(*clip) {
|
||
Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()),
|
||
None => "...."
|
||
};
|
||
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;
|
||
}
|
||
//Ok(Some([x, y, x2, height]))
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// Appears on first run (i.e. if state dir is missing).
|
||
pub struct ArrangerRenameModal<E: Engine> {
|
||
_engine: std::marker::PhantomData<E>,
|
||
done: bool,
|
||
target: ArrangerFocus,
|
||
value: String,
|
||
result: Arc<RwLock<String>>,
|
||
cursor: usize
|
||
}
|
||
|
||
impl<E: Engine> ArrangerRenameModal<E> {
|
||
pub fn new (target: ArrangerFocus, value: &Arc<RwLock<String>>) -> Self {
|
||
Self {
|
||
_engine: Default::default(),
|
||
done: false,
|
||
target,
|
||
value: value.read().unwrap().clone(),
|
||
cursor: value.read().unwrap().len(),
|
||
result: value.clone(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Widget for ArrangerRenameModal<Tui> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
let y = area.y() + area.h() / 2;
|
||
let bg_area = [1, y - 1, area.w() - 2, 3];
|
||
to.fill_bg(bg_area, Nord::BG0);
|
||
Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area));
|
||
let label = match self.target {
|
||
ArrangerFocus::Mix => "Rename project:",
|
||
ArrangerFocus::Track(_) => "Rename track:",
|
||
ArrangerFocus::Scene(_) => "Rename scene:",
|
||
ArrangerFocus::Clip(_, _) => "Rename clip:",
|
||
};
|
||
let style = Some(Style::default().not_bold().white().not_dim());
|
||
to.blit(&label, area.x() + 3, y, style);
|
||
let style = Some(Style::default().bold().white().not_dim());
|
||
to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style);
|
||
let style = Some(Style::default().bold().white().not_dim().reversed());
|
||
to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style);
|
||
//Ok(Some(area))
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
TuiEvent::Input(Event::Key(k)) => {
|
||
match k.code {
|
||
KeyCode::Esc => {
|
||
self.exit();
|
||
},
|
||
KeyCode::Enter => {
|
||
*self.result.write().unwrap() = self.value.clone();
|
||
self.exit();
|
||
},
|
||
KeyCode::Left => {
|
||
self.cursor = self.cursor.saturating_sub(1);
|
||
},
|
||
KeyCode::Right => {
|
||
self.cursor = self.value.len().min(self.cursor + 1)
|
||
},
|
||
KeyCode::Backspace => {
|
||
let last = self.value.len().saturating_sub(1);
|
||
self.value = format!("{}{}",
|
||
&self.value[0..self.cursor.min(last)],
|
||
&self.value[self.cursor.min(last)..last]
|
||
);
|
||
self.cursor = self.cursor.saturating_sub(1)
|
||
}
|
||
KeyCode::Char(c) => {
|
||
self.value.insert(self.cursor, c);
|
||
self.cursor = self.value.len().min(self.cursor + 1)
|
||
},
|
||
_ => {}
|
||
}
|
||
Ok(Some(true))
|
||
},
|
||
_ => Ok(None),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
||
fn exited (&self) -> bool {
|
||
self.done
|
||
}
|
||
fn exit (&mut self) {
|
||
self.done = true
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// Phrase editor.
|
||
pub struct Sequencer<E: Engine> {
|
||
pub name: Arc<RwLock<String>>,
|
||
pub mode: bool,
|
||
pub focused: bool,
|
||
pub entered: bool,
|
||
|
||
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
||
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
||
pub buffer: BigBuffer,
|
||
pub keys: Buffer,
|
||
/// Highlight input keys
|
||
pub keys_in: [bool; 128],
|
||
/// Highlight output keys
|
||
pub keys_out: [bool; 128],
|
||
|
||
pub now: usize,
|
||
pub ppq: usize,
|
||
pub note_axis: FixedAxis<usize>,
|
||
pub time_axis: ScaledAxis<usize>,
|
||
/// Play input through output.
|
||
pub monitoring: bool,
|
||
/// Write input to sequence.
|
||
pub recording: bool,
|
||
/// Overdub input to sequence.
|
||
pub overdub: bool,
|
||
/// Map: tick -> MIDI events at tick
|
||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||
/// Phrase selector
|
||
pub sequence: Option<usize>,
|
||
/// Output from current sequence.
|
||
pub midi_out: Option<Port<MidiOut>>,
|
||
/// MIDI output buffer
|
||
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
||
/// Send all notes off
|
||
pub reset: bool, // TODO?: after Some(nframes)
|
||
/// Highlight keys on piano roll.
|
||
pub notes_in: [bool;128],
|
||
/// Highlight keys on piano roll.
|
||
pub notes_out: [bool;128],
|
||
}
|
||
|
||
impl<E: Engine> Sequencer<E> {
|
||
pub fn new (name: &str) -> Self {
|
||
Self {
|
||
name: Arc::new(RwLock::new(name.into())),
|
||
monitoring: false,
|
||
recording: false,
|
||
overdub: true,
|
||
phrases: vec![],
|
||
sequence: None,
|
||
midi_out: None,
|
||
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
||
reset: true,
|
||
notes_in: [false;128],
|
||
notes_out: [false;128],
|
||
buffer: Default::default(),
|
||
keys: keys_vert(),
|
||
entered: false,
|
||
focused: false,
|
||
mode: false,
|
||
keys_in: [false;128],
|
||
keys_out: [false;128],
|
||
phrase: None,
|
||
now: 0,
|
||
ppq: 96,
|
||
transport: None,
|
||
note_axis: FixedAxis { start: 12, point: Some(36) },
|
||
time_axis: ScaledAxis { start: 0, scale: 24, point: Some(0) },
|
||
}
|
||
}
|
||
pub fn toggle_monitor (&mut self) {
|
||
self.monitoring = !self.monitoring;
|
||
}
|
||
pub fn toggle_record (&mut self) {
|
||
self.recording = !self.recording;
|
||
}
|
||
pub fn toggle_overdub (&mut self) {
|
||
self.overdub = !self.overdub;
|
||
}
|
||
pub fn process (
|
||
&mut self,
|
||
input: Option<MidiIter>,
|
||
timebase: &Arc<Timebase>,
|
||
playing: Option<TransportState>,
|
||
started: Option<(usize, usize)>,
|
||
quant: usize,
|
||
reset: bool,
|
||
scope: &ProcessScope,
|
||
(frame0, frames): (usize, usize),
|
||
(_usec0, _usecs): (usize, usize),
|
||
period: f64,
|
||
) {
|
||
if self.midi_out.is_some() {
|
||
// Clear the section of the output buffer that we will be using
|
||
for frame in &mut self.midi_out_buf[0..frames] {
|
||
frame.clear();
|
||
}
|
||
// Emit "all notes off" at start of buffer if requested
|
||
if self.reset {
|
||
all_notes_off(&mut self.midi_out_buf);
|
||
self.reset = false;
|
||
} else if reset {
|
||
all_notes_off(&mut self.midi_out_buf);
|
||
}
|
||
}
|
||
if let (
|
||
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
|
||
) = (
|
||
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
|
||
) {
|
||
phrase.read().map(|phrase|{
|
||
if self.midi_out.is_some() {
|
||
phrase.process_out(
|
||
&mut self.midi_out_buf,
|
||
&mut self.notes_out,
|
||
timebase,
|
||
(frame0.saturating_sub(start_frame), frames, period)
|
||
);
|
||
}
|
||
}).unwrap();
|
||
let mut phrase = phrase.write().unwrap();
|
||
let length = phrase.length;
|
||
// Monitor and record input
|
||
if input.is_some() && (self.recording || self.monitoring) {
|
||
// For highlighting keys and note repeat
|
||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||
match event {
|
||
LiveEvent::Midi { message, .. } => {
|
||
if self.monitoring {
|
||
self.midi_out_buf[frame].push(bytes.to_vec())
|
||
}
|
||
if self.recording {
|
||
phrase.record_event({
|
||
let pulse = timebase.frame_to_pulse(
|
||
(frame0 + frame - start_frame) as f64
|
||
);
|
||
let quantized = (
|
||
pulse / quant as f64
|
||
).round() as usize * quant;
|
||
let looped = quantized % length;
|
||
looped
|
||
}, message);
|
||
}
|
||
match message {
|
||
MidiMessage::NoteOn { key, .. } => {
|
||
self.notes_in[key.as_int() as usize] = true;
|
||
}
|
||
MidiMessage::NoteOff { key, .. } => {
|
||
self.notes_in[key.as_int() as usize] = false;
|
||
},
|
||
_ => {}
|
||
}
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
|
||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||
self.process_monitor_event(frame, &event, bytes)
|
||
}
|
||
}
|
||
if let Some(out) = &mut self.midi_out {
|
||
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
|
||
match event {
|
||
LiveEvent::Midi { message, .. } => {
|
||
self.write_to_output_buffer(frame, bytes);
|
||
self.process_monitor_message(&message);
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
|
||
self.midi_out_buf[frame].push(bytes.to_vec());
|
||
}
|
||
|
||
#[inline]
|
||
fn process_monitor_message (&mut self, message: &MidiMessage) {
|
||
match message {
|
||
MidiMessage::NoteOn { key, .. } => {
|
||
self.notes_in[key.as_int() as usize] = true;
|
||
}
|
||
MidiMessage::NoteOff { key, .. } => {
|
||
self.notes_in[key.as_int() as usize] = false;
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
/// 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>>>) -> Usually<()> {
|
||
self.phrase = phrase.map(Clone::clone);
|
||
if let Some(ref phrase) = self.phrase {
|
||
let width = usize::MAX.min(phrase.read().unwrap().length);
|
||
let mut buffer = BigBuffer::new(width, 64);
|
||
let phrase = phrase.read().unwrap();
|
||
fill_seq_bg(&mut buffer, phrase.length, self.ppq)?;
|
||
fill_seq_fg(&mut buffer, &phrase)?;
|
||
self.buffer = buffer;
|
||
} else {
|
||
self.buffer = Default::default();
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(crate) fn style_focus (&self) -> Option<Style> {
|
||
Some(if self.focused {
|
||
Style::default().green().not_dim()
|
||
} else {
|
||
Style::default().green().dim()
|
||
})
|
||
}
|
||
|
||
pub(crate) fn style_timer_step (now: usize, step: usize, next_step: usize) -> Style {
|
||
if step <= now && now < next_step {
|
||
Style::default().yellow().bold().not_dim()
|
||
} else {
|
||
Style::default()
|
||
}
|
||
}
|
||
|
||
pub fn index_to_color (&self, index: u16, default: Color) -> Color {
|
||
let index = index as usize;
|
||
if self.keys_in[index] && self.keys_out[index] {
|
||
Color::Yellow
|
||
} else if self.keys_in[index] {
|
||
Color::Red
|
||
} else if self.keys_out[index] {
|
||
Color::Green
|
||
} else {
|
||
default
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Add "all notes off" to the start of a buffer.
|
||
pub fn all_notes_off (output: &mut MIDIChunk) {
|
||
let mut buf = vec![];
|
||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||
evt.write(&mut buf).unwrap();
|
||
output[0].push(buf);
|
||
}
|
||
|
||
/// Return boxed iterator of MIDI events
|
||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||
Box::new(input.map(|RawMidi { time, bytes }|(
|
||
time as usize,
|
||
LiveEvent::parse(bytes).unwrap(),
|
||
bytes
|
||
)))
|
||
}
|
||
|
||
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
||
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
|
||
for time in 0..frames {
|
||
for event in output[time].iter() {
|
||
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
||
.expect(&format!("{event:?}"));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// MIDI message serialized to bytes
|
||
pub type MIDIMessage = Vec<u8>;
|
||
|
||
/// Collection of serialized MIDI messages
|
||
pub type MIDIChunk = [Vec<MIDIMessage>];
|
||
|
||
impl Sequencer<Tui> {
|
||
const H_KEYS_OFFSET: usize = 5;
|
||
}
|
||
|
||
impl Content for Sequencer<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
Split::right(move |add|{
|
||
add(&Split::down(|add|{
|
||
add(&SequenceName(&self))?;
|
||
add(&SequenceRange)?;
|
||
add(&SequenceLoopRange)?;
|
||
add(&SequenceNoteRange)?;
|
||
Ok(())
|
||
}))?;
|
||
add(&Layers::new(|add|{
|
||
add(&SequenceKeys(&self))?;
|
||
add(&self.phrase.as_ref().map(|phrase|SequenceTimer(&self, phrase.clone())))?;
|
||
add(&SequenceNotes(&self))?;
|
||
add(&SequenceCursor(&self))?;
|
||
add(&SequenceZoom(&self))
|
||
}))
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Handle<Tui> for Sequencer<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
// NONE, "seq_cursor_up", "move cursor up", |sequencer: &mut Sequencer| {
|
||
key!(KeyCode::Up) => {
|
||
match self.entered {
|
||
true => { self.note_axis.point_dec(); },
|
||
false => { self.note_axis.start_dec(); },
|
||
}
|
||
Ok(Some(true))
|
||
},
|
||
// NONE, "seq_cursor_down", "move cursor down", |self: &mut Sequencer| {
|
||
key!(KeyCode::Down) => {
|
||
match self.entered {
|
||
true => { self.note_axis.point_inc(); },
|
||
false => { self.note_axis.start_inc(); },
|
||
}
|
||
Ok(Some(true))
|
||
},
|
||
// NONE, "seq_cursor_left", "move cursor up", |self: &mut Sequencer| {
|
||
key!(KeyCode::Left) => {
|
||
match self.entered {
|
||
true => { self.time_axis.point_dec(); },
|
||
false => { self.time_axis.start_dec(); },
|
||
}
|
||
Ok(Some(true))
|
||
},
|
||
// NONE, "seq_cursor_right", "move cursor up", |self: &mut Sequencer| {
|
||
key!(KeyCode::Right) => {
|
||
match self.entered {
|
||
true => { self.time_axis.point_inc(); },
|
||
false => { self.time_axis.start_inc(); },
|
||
}
|
||
Ok(Some(true))
|
||
},
|
||
// NONE, "seq_mode_switch", "switch the display mode", |self: &mut Sequencer| {
|
||
key!(KeyCode::Char('`')) => {
|
||
self.mode = !self.mode;
|
||
Ok(Some(true))
|
||
},
|
||
_ => Ok(None)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn nth_octave (index: u16) -> &'static str {
|
||
match index {
|
||
0 => "-1",
|
||
1 => "0",
|
||
2 => "1",
|
||
3 => "2",
|
||
4 => "3",
|
||
5 => "4",
|
||
6 => "5",
|
||
7 => "6",
|
||
8 => "7",
|
||
9 => "8",
|
||
10 => "9",
|
||
_ => unreachable!()
|
||
}
|
||
}
|
||
|
||
fn key_colors (index: u16) -> (Color, Color) {
|
||
match index % 6 {
|
||
0 => (Color::White, Color::Black),
|
||
1 => (Color::White, Color::Black),
|
||
2 => (Color::White, Color::White),
|
||
3 => (Color::Black, Color::White),
|
||
4 => (Color::Black, Color::White),
|
||
5 => (Color::Black, Color::White),
|
||
_ => unreachable!()
|
||
}
|
||
}
|
||
|
||
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) -> Usually<()> {
|
||
for x in 0..buf.width {
|
||
if x as usize >= length {
|
||
break
|
||
}
|
||
let style = Style::default();
|
||
buf.get_mut(x, 0).map(|cell|{
|
||
cell.set_char('-');
|
||
cell.set_style(style);
|
||
});
|
||
for y in 0 .. buf.height {
|
||
buf.get_mut(x, y).map(|cell|{
|
||
cell.set_char(char_seq_bg(ppq, x as u16));
|
||
cell.set_fg(Color::Gray);
|
||
cell.modifier = Modifier::DIM;
|
||
});
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn char_seq_bg (ppq: usize, x: u16) -> char {
|
||
if ppq == 0 {
|
||
'·'
|
||
} else if x % (4 * ppq as u16) == 0 {
|
||
'│'
|
||
} else if x % ppq as u16 == 0 {
|
||
'╎'
|
||
} else {
|
||
'·'
|
||
}
|
||
}
|
||
|
||
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> {
|
||
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/2 {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
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);
|
||
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));
|
||
},
|
||
_ => {}
|
||
}
|
||
});
|
||
buffer
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceName<'a>(&'a Sequencer<Tui>);
|
||
|
||
impl<'a> Widget for SequenceName<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let [x, y, ..] = to.area();
|
||
let frame = [x, y, 10, 4];
|
||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(frame))?;
|
||
to.blit(&"Name:", x + 1, y + 1, Some(STYLE_LABEL));
|
||
to.blit(&*self.0.name.read().unwrap(), x + 1, y + 2, Some(STYLE_VALUE));
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceRange;
|
||
|
||
impl Widget for SequenceRange {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let [x, y, ..] = to.area();
|
||
let frame = [x, y, 10, 6];
|
||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(frame))?;
|
||
to.blit(&"Start: ", x + 1, y + 1, Some(STYLE_LABEL));
|
||
to.blit(&" 1.1.1", x + 1, y + 2, Some(STYLE_VALUE));
|
||
to.blit(&"End: ", x + 1, y + 3, Some(STYLE_LABEL));
|
||
to.blit(&" 2.1.1", x + 1, y + 4, Some(STYLE_VALUE));
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceLoopRange;
|
||
|
||
impl Widget for SequenceLoopRange {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let [x, y, ..] = to.area();
|
||
let range = [x, y, 10, 7];
|
||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(range))?;
|
||
to.blit(&"Loop [ ]", x + 1, y + 1, Some(STYLE_LABEL));
|
||
to.blit(&"From: ", x + 1, y + 2, Some(STYLE_LABEL));
|
||
to.blit(&" 1.1.1", x + 1, y + 3, Some(STYLE_VALUE));
|
||
to.blit(&"Length: ", x + 1, y + 4, Some(STYLE_LABEL));
|
||
to.blit(&" 1.0.0", x + 1, y + 5, Some(STYLE_VALUE));
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceNoteRange;
|
||
|
||
impl Widget for SequenceNoteRange {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let [x, y, ..] = to.area();
|
||
let range = [x, y, 10, 9];
|
||
Lozenge(Style::default().fg(Nord::BG2)).draw(to.with_rect(range))?;
|
||
to.blit(&"Notes: ", x + 1, y + 1, Some(STYLE_LABEL));
|
||
to.blit(&"C#0-C#9 ", x + 1, y + 2, Some(STYLE_VALUE));
|
||
to.blit(&"[ /2 ]", x + 1, y + 3, Some(STYLE_LABEL));
|
||
to.blit(&"[ x2 ]", x + 1, y + 4, Some(STYLE_LABEL));
|
||
to.blit(&"[ Rev ]", x + 1, y + 5, Some(STYLE_LABEL));
|
||
to.blit(&"[ Inv ]", x + 1, y + 6, Some(STYLE_LABEL));
|
||
to.blit(&"[ Dup ]", x + 1, y + 7, Some(STYLE_LABEL));
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceKeys<'a>(&'a Sequencer<Tui>);
|
||
|
||
impl<'a> Widget for SequenceKeys<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
if area.h() < 2 {
|
||
return Ok(())
|
||
}
|
||
let area = [area.x(), area.y() + 1, 5, area.h() - 2];
|
||
to.buffer_update(area, &|cell, x, y|{
|
||
let y = y + self.0.note_axis.start as u16;
|
||
if x < self.0.keys.area.width && y < self.0.keys.area.height {
|
||
*cell = self.0.keys.get(x, y).clone()
|
||
}
|
||
});
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceNotes<'a>(&'a Sequencer<Tui>);
|
||
|
||
impl<'a> Widget for SequenceNotes<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
if area.h() < 2 {
|
||
return Ok(())//Some(area))
|
||
}
|
||
let area = [
|
||
area.x() + Sequencer::H_KEYS_OFFSET as u16,
|
||
area.y() + 1,
|
||
area.w().saturating_sub(Sequencer::H_KEYS_OFFSET as u16),
|
||
area.h().saturating_sub(2),
|
||
];
|
||
to.buffer_update(area, &move |cell, x, y|{
|
||
let src_x = ((x as usize + self.0.time_axis.start) * self.0.time_axis.scale) as usize;
|
||
let src_y = (y as usize + self.0.note_axis.start) as usize;
|
||
if src_x < self.0.buffer.width && src_y < self.0.buffer.height - 1 {
|
||
let src = self.0.buffer.get(src_x, self.0.buffer.height - src_y);
|
||
src.map(|src|{
|
||
cell.set_symbol(src.symbol());
|
||
cell.set_fg(src.fg);
|
||
});
|
||
}
|
||
});
|
||
Ok(())//Some(area))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceCursor<'a>(&'a Sequencer<Tui>);
|
||
|
||
impl<'a> Widget for SequenceCursor<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
if let (Some(time), Some(note)) = (self.0.time_axis.point, self.0.note_axis.point) {
|
||
let x = area.x() + Sequencer::H_KEYS_OFFSET as u16 + time as u16;
|
||
let y = area.y() + 1 + note as u16 / 2;
|
||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||
to.blit(&c, x, y, self.0.style_focus());
|
||
Ok(())
|
||
} else {
|
||
//Ok(Some([0,0,0,0]))
|
||
Ok(())
|
||
}
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
struct SequenceZoom<'a>(&'a Sequencer<Tui>);
|
||
|
||
impl<'a> Widget for SequenceZoom<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
let quant = ppq_to_name(self.0.time_axis.scale);
|
||
let quant_x = area.x() + area.w() - 1 - quant.len() as u16;
|
||
let quant_y = area.y() + area.h() - 2;
|
||
to.blit(&quant, quant_x, quant_y, self.0.style_focus());
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
struct SequenceTimer<'a>(&'a Sequencer<Tui>, Arc<RwLock<Phrase>>);
|
||
|
||
impl<'a> Widget for SequenceTimer<'a> {
|
||
type Engine = Tui;
|
||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||
todo!()
|
||
}
|
||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||
let area = to.area();
|
||
let phrase = self.1.read().unwrap();
|
||
let (time0, time_z, now) = (
|
||
self.0.time_axis.start, self.0.time_axis.scale, self.0.now % phrase.length
|
||
);
|
||
let [x, _, width, _] = area;
|
||
let x2 = x as usize + Sequencer::H_KEYS_OFFSET;
|
||
let x3 = x as usize + width as usize;
|
||
for x in x2..x3 {
|
||
let step = (time0 + x2) * time_z;
|
||
let next_step = (time0 + x2 + 1) * time_z;
|
||
let style = Sequencer::<Tui>::style_timer_step(now, step as usize, next_step as usize);
|
||
to.blit(&"-", x as u16, area.y(), Some(style));
|
||
}
|
||
//return Ok(Some([area.x(), area.y(), area.w(), 1]))
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// A collection of phrases to play on each track.
|
||
#[derive(Default)]
|
||
pub struct Scene {
|
||
pub name: Arc<RwLock<String>>,
|
||
pub clips: Vec<Option<usize>>,
|
||
}
|
||
|
||
impl Scene {
|
||
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||
let mut name = None;
|
||
let mut clips = vec![];
|
||
edn!(edn in args {
|
||
Edn::Map(map) => {
|
||
let key = map.get(&Edn::Key(":name"));
|
||
if let Some(Edn::Str(n)) = key {
|
||
name = Some(*n);
|
||
} else {
|
||
panic!("unexpected key in scene '{name:?}': {key:?}")
|
||
}
|
||
},
|
||
Edn::Symbol("_") => {
|
||
clips.push(None);
|
||
},
|
||
Edn::Int(i) => {
|
||
clips.push(Some(*i as usize));
|
||
},
|
||
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
||
});
|
||
let scene = Self::new(name.unwrap_or(""), clips);
|
||
Ok(scene)
|
||
}
|
||
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
|
||
let name = Arc::new(RwLock::new(name.as_ref().into()));
|
||
let clips = clips.as_ref().iter().map(|x|x.clone()).collect();
|
||
Self { name, clips, }
|
||
}
|
||
/// Returns the pulse length of the longest phrase in the scene
|
||
pub fn pulses <E: Engine> (&self, tracks: &[Sequencer<E>]) -> usize {
|
||
self.clips.iter().enumerate()
|
||
.filter_map(|(i, c)|c
|
||
.map(|c|tracks
|
||
.get(i)
|
||
.map(|track|track
|
||
.phrases
|
||
.get(c))))
|
||
.filter_map(|p|p)
|
||
.filter_map(|p|p)
|
||
.fold(0, |a, p|a.max(p.read().unwrap().length))
|
||
}
|
||
/// Returns true if all phrases in the scene are currently playing
|
||
pub fn is_playing <E: Engine> (&self, tracks: &[Sequencer<E>]) -> bool {
|
||
self.clips.iter().enumerate()
|
||
.all(|(track_index, phrase_index)|match phrase_index {
|
||
Some(i) => tracks
|
||
.get(track_index)
|
||
.map(|track|track.sequence == Some(*i))
|
||
.unwrap_or(false),
|
||
None => true
|
||
})
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
#[derive(Debug)]
|
||
/// A MIDI sequence.
|
||
pub struct Phrase {
|
||
pub name: Arc<RwLock<String>>,
|
||
pub length: usize,
|
||
pub notes: PhraseData,
|
||
pub looped: Option<(usize, usize)>,
|
||
/// All notes are displayed with minimum length
|
||
pub percussive: bool
|
||
}
|
||
|
||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||
|
||
impl Default for Phrase {
|
||
fn default () -> Self {
|
||
Self::new("", 0, None)
|
||
}
|
||
}
|
||
|
||
impl Phrase {
|
||
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
||
Self {
|
||
name: Arc::new(RwLock::new(name.into())),
|
||
length,
|
||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||
looped: Some((0, length)),
|
||
percussive: true,
|
||
}
|
||
}
|
||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||
if pulse >= self.length {
|
||
panic!("extend phrase first")
|
||
}
|
||
self.notes[pulse].push(message);
|
||
}
|
||
/// Check if a range `start..end` contains MIDI Note On `k`
|
||
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||
//panic!("{:?} {start} {end}", &self);
|
||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||
for event in events.iter() {
|
||
match event {
|
||
MidiMessage::NoteOn {key,..} => {
|
||
if *key == k {
|
||
return true
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
/// Write a chunk of MIDI events to an output port.
|
||
pub fn process_out (
|
||
&self,
|
||
output: &mut MIDIChunk,
|
||
notes_on: &mut [bool;128],
|
||
timebase: &Arc<Timebase>,
|
||
(frame0, frames, _): (usize, usize, f64),
|
||
) {
|
||
let mut buf = Vec::with_capacity(8);
|
||
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
||
frame0, frame0 + frames
|
||
) {
|
||
let tick = tick % self.length;
|
||
for message in self.notes[tick].iter() {
|
||
buf.clear();
|
||
let channel = 0.into();
|
||
let message = *message;
|
||
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
||
output[time as usize].push(buf.clone());
|
||
match message {
|
||
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
||
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pub fn from_edn <'e> (ppq: usize, args: &[Edn<'e>]) -> Usually<Self> {
|
||
let mut phrase = Self::default();
|
||
let mut name = String::new();
|
||
let mut beats = 0usize;
|
||
let mut steps = 0usize;
|
||
edn!(edn in args {
|
||
Edn::Map(map) => {
|
||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||
name = String::from(*n);
|
||
}
|
||
if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) {
|
||
beats = *b as usize;
|
||
phrase.length = ppq * beats;
|
||
for _ in phrase.notes.len()..phrase.length {
|
||
phrase.notes.push(Vec::with_capacity(16))
|
||
}
|
||
}
|
||
if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) {
|
||
steps = *s as usize;
|
||
}
|
||
},
|
||
Edn::List(args) => {
|
||
let time = (match args.get(0) {
|
||
Some(Edn::Key(text)) => text[1..].parse::<f64>()?,
|
||
Some(Edn::Int(i)) => *i as f64,
|
||
Some(Edn::Double(f)) => f64::from(*f),
|
||
_ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)),
|
||
} * beats as f64 * ppq as f64 / steps as f64) as usize;
|
||
for edn in args[1..].iter() {
|
||
match edn {
|
||
Edn::List(args) => if let (
|
||
Some(Edn::Int(key)),
|
||
Some(Edn::Int(vel)),
|
||
) = (
|
||
args.get(0),
|
||
args.get(1),
|
||
) {
|
||
let (key, vel) = (
|
||
u7::from((*key as u8).min(127)),
|
||
u7::from((*vel as u8).min(127)),
|
||
);
|
||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel })
|
||
} else {
|
||
panic!("unexpected list in phrase '{name}'")
|
||
},
|
||
_ => panic!("unexpected in phrase '{name}': {edn:?}")
|
||
}
|
||
}
|
||
},
|
||
_ => panic!("unexpected in phrase '{name}': {edn:?}"),
|
||
});
|
||
*phrase.name.write().unwrap() = name;
|
||
Ok(phrase)
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
/// Stores and displays time-related state.
|
||
pub struct TransportToolbar<E: Engine> {
|
||
/// Enable metronome?
|
||
pub metronome: bool,
|
||
/// Current sample rate, tempo, and PPQ.
|
||
pub timebase: Arc<Timebase>,
|
||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||
pub jack: Option<JackClient>,
|
||
/// JACK transport handle.
|
||
pub transport: Option<Transport>,
|
||
/// Quantization factor
|
||
/// Global frame and usec at which playback started
|
||
pub started: Option<(usize, usize)>,
|
||
|
||
pub focused: bool,
|
||
pub focus: usize,
|
||
pub playing: TransportPlayPauseButton<E>,
|
||
pub bpm: TransportBPM<E>,
|
||
pub quant: TransportQuantize<E>,
|
||
pub sync: TransportSync<E>,
|
||
pub clock: TransportClock<E>,
|
||
}
|
||
|
||
impl<E: Engine> TransportToolbar<E> {
|
||
pub fn standalone () -> Usually<Arc<RwLock<Self>>> where Self: 'static {
|
||
let mut transport = Self::new(None);
|
||
transport.focused = true;
|
||
let jack = JackClient::Inactive(
|
||
Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0
|
||
);
|
||
transport.transport = Some(jack.transport());
|
||
transport.playing.transport = Some(jack.transport());
|
||
let transport = Arc::new(RwLock::new(transport));
|
||
transport.write().unwrap().jack = Some(
|
||
jack.activate(
|
||
&transport.clone(),
|
||
|state: &Arc<RwLock<TransportToolbar<E>>>, client, scope| {
|
||
state.write().unwrap().process(client, scope)
|
||
}
|
||
)?
|
||
);
|
||
Ok(transport)
|
||
}
|
||
pub fn new (transport: Option<Transport>) -> Self {
|
||
let timebase = Arc::new(Timebase::default());
|
||
Self {
|
||
focused: false,
|
||
focus: 0,
|
||
|
||
playing: TransportPlayPauseButton {
|
||
_engine: Default::default(),
|
||
transport: None,
|
||
value: Some(TransportState::Stopped),
|
||
focused: true
|
||
},
|
||
bpm: TransportBPM {
|
||
_engine: Default::default(),
|
||
value: timebase.bpm(),
|
||
focused: false
|
||
},
|
||
quant: TransportQuantize {
|
||
_engine: Default::default(),
|
||
value: 24,
|
||
focused: false
|
||
},
|
||
sync: TransportSync {
|
||
_engine: Default::default(),
|
||
value: timebase.ppq() as usize * 4,
|
||
focused: false
|
||
},
|
||
clock: TransportClock {
|
||
_engine: Default::default(),
|
||
frame: 0,
|
||
pulse: 0,
|
||
ppq: 0,
|
||
usecs: 0,
|
||
focused: false
|
||
},
|
||
|
||
transport,
|
||
timebase,
|
||
metronome: false,
|
||
started: None,
|
||
jack: None,
|
||
}
|
||
}
|
||
pub fn toggle_play (&mut self) -> Usually<()> {
|
||
self.playing.toggle()?;
|
||
Ok(())
|
||
}
|
||
pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) {
|
||
let CycleTimes {
|
||
current_frames,
|
||
current_usecs,
|
||
next_usecs,
|
||
period_usecs
|
||
} = scope.cycle_times().unwrap();
|
||
let chunk_size = scope.n_frames() as usize;
|
||
let transport = self.transport.as_ref().unwrap().query().unwrap();
|
||
self.clock.frame = transport.pos.frame() as usize;
|
||
let mut reset = false;
|
||
if self.playing.value != Some(transport.state) {
|
||
match transport.state {
|
||
TransportState::Rolling => {
|
||
self.started = Some((
|
||
current_frames as usize,
|
||
current_usecs as usize,
|
||
));
|
||
},
|
||
TransportState::Stopped => {
|
||
self.started = None;
|
||
reset = true;
|
||
},
|
||
_ => {}
|
||
}
|
||
}
|
||
self.playing.value = Some(transport.state);
|
||
(
|
||
reset,
|
||
current_frames as usize,
|
||
chunk_size as usize,
|
||
current_usecs as usize,
|
||
next_usecs as usize,
|
||
period_usecs as f64
|
||
)
|
||
}
|
||
pub fn bpm (&self) -> usize {
|
||
self.timebase.bpm() as usize
|
||
}
|
||
pub fn ppq (&self) -> usize {
|
||
self.timebase.ppq() as usize
|
||
}
|
||
pub fn pulse (&self) -> usize {
|
||
self.timebase.frame_to_pulse(self.clock.frame as f64) as usize
|
||
}
|
||
pub fn usecs (&self) -> usize {
|
||
self.timebase.frame_to_usec(self.clock.frame as f64) as usize
|
||
}
|
||
pub fn quant (&self) -> usize {
|
||
self.quant.value
|
||
}
|
||
pub fn sync (&self) -> usize {
|
||
self.sync.value
|
||
}
|
||
}
|
||
|
||
impl<E: Engine> Audio for TransportToolbar<E> {
|
||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||
self.update(&scope);
|
||
Control::Continue
|
||
}
|
||
}
|
||
|
||
impl Handle<Tui> for TransportToolbar<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
key!(KeyCode::Left) => { self.focus_prev(); },
|
||
key!(KeyCode::Right) => { self.focus_next(); },
|
||
_ => return self.focused_mut().handle(from)
|
||
}
|
||
Ok(Some(true))
|
||
}
|
||
}
|
||
|
||
impl Content for TransportToolbar<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
Split::right(|add|{
|
||
let focus_wrap = |focused, component|Layers::new(move |add|{
|
||
if focused {
|
||
add(&CORNERS)?;
|
||
add(&Background(COLOR_BG1))?;
|
||
}
|
||
add(component)
|
||
});
|
||
add(&focus_wrap(self.focused && self.playing.focused, &self.playing))?;
|
||
add(&focus_wrap(self.focused && self.bpm.focused, &self.bpm))?;
|
||
add(&focus_wrap(self.focused && self.quant.focused, &self.quant))?;
|
||
add(&focus_wrap(self.focused && self.sync.focused, &self.sync))?;
|
||
add(&focus_wrap(self.focused && self.clock.focused, &self.clock))?;
|
||
Ok(())
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Focus<5, Tui> for TransportToolbar<Tui> {
|
||
fn focus (&self) -> usize {
|
||
self.focus
|
||
}
|
||
fn focus_mut (&mut self) -> &mut usize {
|
||
&mut self.focus
|
||
}
|
||
fn focusable (&self) -> [&dyn Focusable<Tui>;5] {
|
||
[
|
||
&self.playing as &dyn Focusable<Tui>,
|
||
&self.bpm as &dyn Focusable<Tui>,
|
||
&self.quant as &dyn Focusable<Tui>,
|
||
&self.sync as &dyn Focusable<Tui>,
|
||
&self.clock as &dyn Focusable<Tui>,
|
||
]
|
||
}
|
||
fn focusable_mut (&mut self) -> [&mut dyn Focusable<Tui>;5] {
|
||
[
|
||
&mut self.playing as &mut dyn Focusable<Tui>,
|
||
&mut self.bpm as &mut dyn Focusable<Tui>,
|
||
&mut self.quant as &mut dyn Focusable<Tui>,
|
||
&mut self.sync as &mut dyn Focusable<Tui>,
|
||
&mut self.clock as &mut dyn Focusable<Tui>,
|
||
]
|
||
}
|
||
}
|
||
|
||
impl Focusable<Tui> for TransportToolbar<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
pub struct TransportPlayPauseButton<E: Engine> {
|
||
pub _engine: PhantomData<E>,
|
||
pub transport: Option<Transport>,
|
||
pub value: Option<TransportState>,
|
||
pub focused: bool
|
||
}
|
||
impl<E: Engine> TransportPlayPauseButton<E> {
|
||
fn toggle (&mut self) -> Usually<()> {
|
||
let transport = self.transport.as_ref().unwrap();
|
||
self.value = match self.value.expect("1st frame has not been processed yet") {
|
||
TransportState::Stopped => {
|
||
transport.start()?;
|
||
Some(TransportState::Starting)
|
||
},
|
||
_ => {
|
||
transport.stop()?;
|
||
transport.locate(0)?;
|
||
Some(TransportState::Stopped)
|
||
},
|
||
};
|
||
Ok(())
|
||
}
|
||
}
|
||
impl Focusable<Tui> for TransportPlayPauseButton<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
impl Handle<Tui> for TransportPlayPauseButton<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
key!(KeyCode::Enter) => self.toggle().map(|_|Some(true)),
|
||
_ => Ok(None)
|
||
}
|
||
}
|
||
}
|
||
impl Content for TransportPlayPauseButton<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
Layers::new(|add|{
|
||
add(&Push::X(1, Min::XY(11, 2, Styled(match self.value {
|
||
Some(TransportState::Stopped) => Some(GRAY_DIM.bold()),
|
||
Some(TransportState::Starting) => Some(GRAY_NOT_DIM_BOLD),
|
||
Some(TransportState::Rolling) => Some(WHITE_NOT_DIM_BOLD),
|
||
_ => unreachable!(),
|
||
}, match self.value {
|
||
Some(TransportState::Rolling) => "▶ PLAYING",
|
||
Some(TransportState::Starting) => "READY ...",
|
||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||
_ => unreachable!(),
|
||
}))))?;
|
||
Ok(())
|
||
})
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
pub struct TransportBPM<E: Engine> {
|
||
pub _engine: PhantomData<E>,
|
||
pub value: f64,
|
||
pub focused: bool
|
||
}
|
||
impl Focusable<Tui> for TransportBPM<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
impl Handle<Tui> for TransportBPM<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
key!(KeyCode::Char(',')) => { self.value -= 1.0; },
|
||
key!(KeyCode::Char('.')) => { self.value += 1.0; },
|
||
key!(KeyCode::Char('<')) => { self.value -= 0.001; },
|
||
key!(KeyCode::Char('>')) => { self.value += 0.001; },
|
||
_ => return Ok(None)
|
||
}
|
||
Ok(Some(true))
|
||
}
|
||
}
|
||
impl Content for TransportBPM<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
let Self { value, .. } = self;
|
||
Outset::X(1u16, Split::down(move |add|{
|
||
add(&"BPM")?;
|
||
add(&format!("{}.{:03}", *value as usize, (value * 1000.0) % 1000.0).as_str())
|
||
}))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
pub struct TransportQuantize<E: Engine> {
|
||
pub _engine: PhantomData<E>,
|
||
pub value: usize,
|
||
pub focused: bool
|
||
}
|
||
impl Focusable<Tui> for TransportQuantize<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
impl Handle<Tui> for TransportQuantize<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
key!(KeyCode::Char(',')) => {
|
||
self.value = prev_note_length(self.value);
|
||
Ok(Some(true))
|
||
},
|
||
key!(KeyCode::Char('.')) => {
|
||
self.value = next_note_length(self.value);
|
||
Ok(Some(true))
|
||
},
|
||
_ => Ok(None)
|
||
}
|
||
}
|
||
}
|
||
impl Content for TransportQuantize<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
let Self { value, .. } = self;
|
||
Outset::X(1u16, Split::down(|add|{
|
||
add(&"QUANT")?;
|
||
add(&ppq_to_name(*value as usize))
|
||
}))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
pub struct TransportSync<E: Engine> {
|
||
pub _engine: PhantomData<E>,
|
||
pub value: usize,
|
||
pub focused: bool
|
||
}
|
||
impl Focusable<Tui> for TransportSync<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
impl Handle<Tui> for TransportSync<Tui> {
|
||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||
match from.event() {
|
||
key!(KeyCode::Char(',')) => {
|
||
self.value = prev_note_length(self.value);
|
||
Ok(Some(true))
|
||
},
|
||
key!(KeyCode::Char('.')) => {
|
||
self.value = next_note_length(self.value);
|
||
Ok(Some(true))
|
||
},
|
||
_ => Ok(None)
|
||
}
|
||
}
|
||
}
|
||
impl Content for TransportSync<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
let Self { value, .. } = self;
|
||
Outset::X(1u16, Split::down(|add|{
|
||
add(&"SYNC")?;
|
||
add(&ppq_to_name(*value as usize))
|
||
}))
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
pub struct TransportClock<E: Engine> {
|
||
pub _engine: PhantomData<E>,
|
||
pub frame: usize,
|
||
pub pulse: usize,
|
||
pub ppq: usize,
|
||
pub usecs: usize,
|
||
pub focused: bool,
|
||
}
|
||
impl Focusable<Tui> for TransportClock<Tui> {
|
||
fn is_focused (&self) -> bool {
|
||
self.focused
|
||
}
|
||
fn set_focused (&mut self, focused: bool) {
|
||
self.focused = focused
|
||
}
|
||
}
|
||
impl Handle<Tui> for TransportClock<Tui> {
|
||
fn handle (&mut self, _: &TuiInput) -> Perhaps<bool> {
|
||
Ok(None)
|
||
}
|
||
}
|
||
impl Content for TransportClock<Tui> {
|
||
type Engine = Tui;
|
||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||
let Self { frame: _frame, pulse, ppq, usecs, .. } = self;
|
||
Layers::new(move|add|{
|
||
let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||
let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1);
|
||
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||
add(&Outset::X(1u16, Split::down(|add|{
|
||
add(&format!("{bars}.{beats}.{pulses:02}").as_str())?;
|
||
add(&format!("{minutes}:{seconds:02}:{msecs:03}").as_str())
|
||
})))
|
||
})
|
||
}
|
||
}
|