wip: refactor: cli, snd, tui

This commit is contained in:
🪞👃🪞 2024-11-09 21:27:04 +01:00
parent 70fc3c97d1
commit 84aacfea82
58 changed files with 416 additions and 191 deletions

View file

@ -0,0 +1,813 @@
use crate::*;
/// Root level object for standalone `tek_arranger`
pub struct ArrangerView<E: Engine> {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
/// Which view is focused
pub focus_cursor: (usize, usize),
/// Whether the focused view is entered
pub entered: bool,
/// Controls the JACK transport.
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
/// Global timebase
pub clock: Arc<TransportTime>,
/// Contains all the sequencers.
pub arrangement: Arrangement<E>,
/// Pool of all phrases in the arrangement
pub phrases: Arc<RwLock<PhrasePool<E>>>,
/// Phrase editor view
pub editor: PhraseEditor<E>,
/// Status bar
pub status: ArrangerStatusBar,
/// Height of arrangement
pub arrangement_split: u16,
/// Width of phrase pool
pub phrases_split: u16,
/// Width and height of app at last render
pub size: Measure<E>,
/// Menu bar
pub menu: MenuBar<E, Self, ArrangerCommand>,
/// Command history
pub history: Vec<ArrangerCommand>,
}
/// Sections in the arranger app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ArrangerFocus {
/// The transport (toolbar) is focused
Transport,
/// The arrangement (grid) is focused
Arrangement,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
/// Status bar for arranger ap
pub enum ArrangerStatusBar {
Transport,
ArrangementMix,
ArrangementTrack,
ArrangementScene,
ArrangementClip,
PhrasePool,
PhraseView,
PhraseEdit,
}
/// Represents the tracks and scenes of the composition.
pub struct Arrangement<E: Engine> {
/// Global JACK client
pub jack: Arc<RwLock<JackClient>>,
/// Global timebase
pub clock: Arc<TransportTime>,
/// Name of arranger
pub name: Arc<RwLock<String>>,
/// Collection of phrases.
pub phrases: Arc<RwLock<PhrasePool<E>>>,
/// Collection of tracks.
pub tracks: Vec<ArrangementTrack>,
/// Collection of scenes.
pub scenes: Vec<Scene>,
/// Currently selected element.
pub selected: ArrangementFocus,
/// Display mode of arranger
pub mode: ArrangementViewMode,
/// Whether the arranger is currently focused
pub focused: bool,
/// Background color of arrangement
pub color: ItemColor,
/// Width and height of arrangement area at last render
pub size: Measure<E>,
/// Whether this is currently in edit mode
pub entered: bool,
}
/// Represents a track in the arrangement
pub struct ArrangementTrack {
/// Name of track
pub name: Arc<RwLock<String>>,
/// Preferred width of track column
pub width: usize,
/// Identifying color of track
pub color: ItemColor,
/// MIDI player/recorder
pub player: PhrasePlayer,
}
#[derive(Default, Debug)]
pub struct Scene {
/// Name of scene
pub name: Arc<RwLock<String>>,
/// Clips in scene, one per track
pub clips: Vec<Option<Arc<RwLock<Phrase>>>>,
/// Identifying color of scene
pub color: ItemColor,
}
#[derive(PartialEq, Clone, Copy)]
/// Represents the current user selection in the arranger
pub enum ArrangementFocus {
/// 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),
}
/// Display mode of arranger
#[derive(PartialEq)]
pub enum ArrangementViewMode {
/// Tracks are rows
Horizontal,
/// Tracks are columns
Vertical(usize),
}
/// Arrangement, rendered vertically (session/grid mode).
pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement<E>, pub usize);
/// Arrangement, rendered horizontally (arrangement/track mode).
pub struct HorizontalArranger<'a, E: Engine>(pub &'a Arrangement<E>);
/// General methods for arranger
impl<E: Engine> Arranger<E> {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
arrangement: Arrangement<E>,
phrases: Arc<RwLock<PhrasePool<E>>>,
) -> Self {
let mut app = Self {
jack: jack.clone(),
focus_cursor: (0, 1),
entered: false,
phrases_split: 20,
arrangement_split: 15,
editor: PhraseEditor::new(),
status: ArrangerStatusBar::ArrangementClip,
transport: transport.clone(),
arrangement,
phrases,
history: vec![],
size: Measure::new(),
clock: if let Some(ref transport) = transport {
transport.read().unwrap().clock.clone()
} else {
Arc::new(TransportTime::default())
},
menu: {
use ArrangerCommand::*;
MenuBar::new()
.add({
use ArrangementCommand::*;
Menu::new("File")
.cmd("n", "New project", Arrangement(New))
.cmd("l", "Load project", Arrangement(Load))
.cmd("s", "Save project", Arrangement(Save))
})
.add({
use TransportCommand::*;
Menu::new("Transport")
.cmd("p", "Play", Transport(Play))
.cmd("s", "Play from start", Transport(PlayFromStart))
.cmd("a", "Pause", Transport(Pause))
})
.add({
use ArrangementCommand::*;
Menu::new("Track")
.cmd("a", "Append new", Arrangement(AddTrack))
.cmd("i", "Insert new", Arrangement(AddTrack))
.cmd("n", "Rename", Arrangement(AddTrack))
.cmd("d", "Delete", Arrangement(AddTrack))
.cmd(">", "Move up", Arrangement(AddTrack))
.cmd("<", "Move down", Arrangement(AddTrack))
})
.add({
use ArrangementCommand::*;
Menu::new("Scene")
.cmd("a", "Append new", Arrangement(AddScene))
.cmd("i", "Insert new", Arrangement(AddTrack))
.cmd("n", "Rename", Arrangement(AddTrack))
.cmd("d", "Delete", Arrangement(AddTrack))
.cmd(">", "Move up", Arrangement(AddTrack))
.cmd("<", "Move down", Arrangement(AddTrack))
})
.add({
use PhrasePoolCommand::*;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
Menu::new("Phrase")
.cmd("a", "Append new", Phrases(Append))
.cmd("i", "Insert new", Phrases(Insert))
.cmd("n", "Rename", Phrases(Rename(Rename::Begin)))
.cmd("t", "Set length", Phrases(Length(Length::Begin)))
.cmd("d", "Delete", Phrases(Delete))
.cmd("l", "Load from MIDI...", Phrases(Import))
.cmd("s", "Save to MIDI...", Phrases(Export))
.cmd(">", "Move up", Phrases(MoveUp))
.cmd("<", "Move down", Phrases(MoveDown))
})
}
};
app.update_focus();
app
}
/// Toggle global play/pause
pub fn toggle_play (&mut self) -> Perhaps<bool> {
match self.transport {
Some(ref mut transport) => { transport.write().unwrap().toggle_play()?; },
None => { return Ok(None) }
}
Ok(Some(true))
}
pub fn next_color (&self) -> ItemColor {
if let ArrangementFocus::Clip(track, scene) = self.arrangement.selected {
let track_color = self.arrangement.tracks[track].color;
let scene_color = self.arrangement.scenes[scene].color;
track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25)
} else {
panic!("could not compute next color")
}
}
/// Focus the editor with the current phrase
pub fn show_phrase (&mut self) { self.editor.show(self.arrangement.phrase().as_ref()); }
/// Focus the editor with the current phrase
pub fn edit_phrase (&mut self) {
if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() {
self.phrases.write().unwrap().append_new(None, Some(self.next_color().into()));
self.arrangement.phrase_put();
}
self.show_phrase();
self.focus(ArrangerFocus::PhraseEditor);
self.editor.entered = true;
}
/// Rename the selected track, scene, or clip
pub fn rename_selected (&mut self) {
let Arrangement { selected, ref scenes, .. } = self.arrangement;
match selected {
ArrangementFocus::Mix => {},
ArrangementFocus::Track(_) => { todo!("rename track"); },
ArrangementFocus::Scene(_) => { todo!("rename scene"); },
ArrangementFocus::Clip(t, s) => if let Some(ref phrase) = scenes[s].clips[t] {
let index = self.phrases.read().unwrap().index_of(&*phrase.read().unwrap());
if let Some(index) = index {
self.focus(ArrangerFocus::PhrasePool);
self.phrases.write().unwrap().phrase = index;
self.phrases.write().unwrap().begin_rename();
}
},
}
}
/// Update status bar
pub fn update_status (&mut self) {
self.status = match self.focused() {
ArrangerFocus::Transport => ArrangerStatusBar::Transport,
ArrangerFocus::Arrangement => match self.arrangement.selected {
ArrangementFocus::Mix => ArrangerStatusBar::ArrangementMix,
ArrangementFocus::Track(_) => ArrangerStatusBar::ArrangementTrack,
ArrangementFocus::Scene(_) => ArrangerStatusBar::ArrangementScene,
ArrangementFocus::Clip(_, _) => ArrangerStatusBar::ArrangementClip,
},
ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool,
ArrangerFocus::PhraseEditor => match self.editor.entered {
true => ArrangerStatusBar::PhraseEdit,
false => ArrangerStatusBar::PhraseView,
},
}
}
}
/// Focus layout of arranger app
impl<E: Engine> FocusGrid for Arranger<E> {
type Item = ArrangerFocus;
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
fn focus_enter (&mut self) {
let focused = self.focused();
if !self.entered {
self.entered = true;
use ArrangerFocus::*;
if let Some(transport) = self.transport.as_ref() {
//transport.write().unwrap().entered = focused == Transport
}
self.arrangement.entered = focused == Arrangement;
self.phrases.write().unwrap().entered = focused == PhrasePool;
self.editor.entered = focused == PhraseEditor;
}
}
fn focus_exit (&mut self) {
if self.entered {
self.entered = false;
self.arrangement.entered = false;
self.editor.entered = false;
self.phrases.write().unwrap().entered = false;
}
}
fn entered (&self) -> Option<ArrangerFocus> {
if self.entered {
Some(self.focused())
} else {
None
}
}
fn layout (&self) -> &[&[ArrangerFocus]] {
use ArrangerFocus::*;
&[
&[Transport, Transport],
&[Arrangement, Arrangement],
&[PhrasePool, PhraseEditor],
]
}
fn update_focus (&mut self) {
use ArrangerFocus::*;
let focused = self.focused();
if let Some(transport) = self.transport.as_ref() {
transport.write().unwrap().focused = focused == Transport
}
self.arrangement.focused = focused == Arrangement;
self.phrases.write().unwrap().focused = focused == PhrasePool;
self.editor.focused = focused == PhraseEditor;
self.update_status();
}
}
/// General methods for arrangement
impl<E: Engine> Arrangement<E> {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
clock: &Arc<TransportTime>,
name: &str,
phrases: &Arc<RwLock<PhrasePool<E>>>
) -> Self {
Self {
jack: jack.clone(),
clock: clock.clone(),
name: Arc::new(RwLock::new(name.into())),
mode: ArrangementViewMode::Vertical(2),
selected: ArrangementFocus::Clip(0, 0),
phrases: phrases.clone(),
scenes: vec![],
tracks: vec![],
focused: false,
color: Color::Rgb(28, 35, 25).into(),
size: Measure::new(),
entered: false,
}
}
fn is_stopped (&self) -> bool {
*self.clock.playing.read().unwrap() == Some(TransportState::Stopped)
}
pub fn activate (&mut self) {
match self.selected {
ArrangementFocus::Scene(s) => {
for (t, track) in self.tracks.iter_mut().enumerate() {
let player = &mut track.player;
let clip = self.scenes[s].clips[t].as_ref();
if player.phrase.is_some() || clip.is_some() {
player.enqueue_next(clip);
}
}
// TODO make transport available here, so that
// activating a scene when stopped starts playback
//if self.is_stopped() {
//self.transport.toggle_play()
//}
},
ArrangementFocus::Clip(t, s) => {
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref());
},
_ => {}
}
}
pub fn delete (&mut self) {
match self.selected {
ArrangementFocus::Track(_) => self.track_del(),
ArrangementFocus::Scene(_) => self.scene_del(),
ArrangementFocus::Clip(_, _) => self.phrase_del(),
_ => {}
}
}
pub fn increment (&mut self) {
match self.selected {
ArrangementFocus::Track(_) => self.track_width_inc(),
ArrangementFocus::Scene(_) => self.scene_next(),
ArrangementFocus::Clip(_, _) => self.phrase_next(),
ArrangementFocus::Mix => self.zoom_in(),
}
}
pub fn decrement (&mut self) {
match self.selected {
ArrangementFocus::Track(_) => self.track_width_dec(),
ArrangementFocus::Scene(_) => self.scene_prev(),
ArrangementFocus::Clip(_, _) => self.phrase_prev(),
ArrangementFocus::Mix => self.zoom_out(),
}
}
pub fn zoom_in (&mut self) {
if let ArrangementViewMode::Vertical(factor) = self.mode {
self.mode = ArrangementViewMode::Vertical(factor + 1)
}
}
pub fn zoom_out (&mut self) {
if let ArrangementViewMode::Vertical(factor) = self.mode {
self.mode = ArrangementViewMode::Vertical(factor.saturating_sub(1))
}
}
pub fn is_first_row (&self) -> bool {
let selected = self.selected;
selected.is_mix() || selected.is_track()
}
pub fn is_last_row (&self) -> bool {
let selected = self.selected;
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
ArrangementFocus::Scene(s) => s == self.scenes.len() - 1,
ArrangementFocus::Clip(_, s) => s == self.scenes.len() - 1,
_ => false
}
}
pub fn toggle_loop (&mut self) {
if let Some(phrase) = self.phrase() {
phrase.write().unwrap().toggle_loop()
}
}
pub fn go_up (&mut self) {
match self.mode {
ArrangementViewMode::Horizontal => self.track_prev(),
_ => self.scene_prev(),
};
}
pub fn go_down (&mut self) {
match self.mode {
ArrangementViewMode::Horizontal => self.track_next(),
_ => self.scene_next(),
};
}
pub fn go_left (&mut self) {
match self.mode {
ArrangementViewMode::Horizontal => self.scene_prev(),
_ => self.track_prev(),
};
}
pub fn go_right (&mut self) {
match self.mode {
ArrangementViewMode::Horizontal => self.scene_next(),
_ => self.track_next(),
};
}
pub fn move_back (&mut self) {
match self.selected {
ArrangementFocus::Scene(s) => {
if s > 0 {
self.scenes.swap(s, s - 1);
self.selected = ArrangementFocus::Scene(s - 1);
}
},
ArrangementFocus::Track(t) => {
if t > 0 {
self.tracks.swap(t, t - 1);
self.selected = ArrangementFocus::Track(t - 1);
// FIXME: also swap clip order in scenes
}
},
_ => todo!("arrangement: move forward")
}
}
pub fn move_forward (&mut self) {
match self.selected {
ArrangementFocus::Scene(s) => {
if s < self.scenes.len().saturating_sub(1) {
self.scenes.swap(s, s + 1);
self.selected = ArrangementFocus::Scene(s + 1);
}
},
ArrangementFocus::Track(t) => {
if t < self.tracks.len().saturating_sub(1) {
self.tracks.swap(t, t + 1);
self.selected = ArrangementFocus::Track(t + 1);
// FIXME: also swap clip order in scenes
}
},
_ => todo!("arrangement: move forward")
}
}
pub fn randomize_color (&mut self) {
match self.selected {
ArrangementFocus::Mix => { self.color = ItemColor::random_dark() },
ArrangementFocus::Track(t) => { self.tracks[t].color = ItemColor::random() },
ArrangementFocus::Scene(s) => { self.scenes[s].color = ItemColor::random() },
ArrangementFocus::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
phrase.write().unwrap().color = ItemColorTriplet::random();
}
}
}
}
/// Methods for tracks in arrangement
impl<E: Engine> Arrangement<E> {
pub fn track (&self) -> Option<&ArrangementTrack> {
self.selected.track().map(|t|self.tracks.get(t)).flatten()
}
pub fn track_mut (&mut self) -> Option<&mut ArrangementTrack> {
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
}
pub fn track_width_inc (&mut self) { self.track_mut().map(|t|t.width_inc()); }
pub fn track_width_dec (&mut self) { self.track_mut().map(|t|t.width_dec()); }
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>, color: Option<ItemColor>
) -> Usually<&mut ArrangementTrack> {
self.tracks.push(name.map_or_else(
|| ArrangementTrack::new(
&self.jack, &self.clock, &self.track_default_name(), color
),
|name| ArrangementTrack::new(
&self.jack, &self.clock, name, color
),
)?);
let index = self.tracks.len() - 1;
Ok(&mut self.tracks[index])
}
pub fn track_del (&mut self) {
if let Some(index) = self.selected.track() {
self.tracks.remove(index);
for scene in self.scenes.iter_mut() {
scene.clips.remove(index);
}
}
}
pub fn track_default_name (&self) -> String {
format!("Track {}", self.tracks.len() + 1)
}
pub fn track_widths (&self) -> Vec<(usize, usize)> {
let mut widths = vec![];
let mut total = 0;
for track in self.tracks.iter() {
let width = track.width;
widths.push((width, total));
total += width;
}
widths.push((0, total));
widths
}
}
/// Methods for scenes in arrangement
impl<E: Engine> Arrangement<E> {
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>, color: Option<ItemColor>
) -> Usually<&mut Scene> {
let clips = vec![None;self.tracks.len()];
let name = name.map(|x|x.to_string()).unwrap_or_else(||self.scene_default_name());
self.scenes.push(Scene::new(name, clips, color));
let index = self.scenes.len() - 1;
Ok(&mut self.scenes[index])
}
pub fn scene_del (&mut self) {
if let Some(index) = self.selected.scene() {
self.scenes.remove(index);
}
}
pub fn scene_default_name (&self) -> String {
format!("Scene {}", self.scenes.len() + 1)
}
}
/// Methods for phrases in arrangement
impl<E: Engine> Arrangement<E> {
pub fn sequencer (&self) -> Option<&ArrangementTrack> {
self.selected.track().map(|track|self.tracks.get(track)).flatten()
}
pub fn sequencer_mut (&mut self) -> Option<&mut ArrangementTrack> {
self.selected.track().map(|track|self.tracks.get_mut(track)).flatten()
}
pub fn phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.scene()?.clips.get(self.selected.track()?)?.clone()
}
pub fn phrase_del (&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)))
.map(|(track_index, _)|scene_index
.and_then(|index|self.scenes.get_mut(index))
.map(|scene|scene.clips[track_index] = None));
}
pub fn phrase_put (&mut self) {
if let ArrangementFocus::Clip(track, scene) = self.selected {
self.scenes[scene].clips[track] = Some(self.phrases.read().unwrap().phrase().clone());
}
}
pub fn phrase_get (&mut self) {
if let ArrangementFocus::Clip(track, scene) = self.selected {
if let Some(phrase) = &self.scenes[scene].clips[track] {
let mut phrases = self.phrases.write().unwrap();
if let Some(index) = phrases.index_of(&*phrase.read().unwrap()) {
phrases.phrase = index;
}
}
}
}
pub fn phrase_next (&mut self) {
if let ArrangementFocus::Clip(track, scene) = self.selected {
if let Some(ref mut phrase) = self.scenes[scene].clips[track] {
let phrases = self.phrases.read().unwrap();
let index = phrases.index_of(&*phrase.read().unwrap());
if let Some(index) = index {
if index < phrases.phrases.len().saturating_sub(1) {
*phrase = phrases.phrases[index + 1].clone();
}
}
}
}
}
pub fn phrase_prev (&mut self) {
if let ArrangementFocus::Clip(track, scene) = self.selected {
if let Some(ref mut phrase) = self.scenes[scene].clips[track] {
let phrases = self.phrases.read().unwrap();
let index = phrases.index_of(&*phrase.read().unwrap());
if let Some(index) = index {
if index > 0 {
*phrase = phrases.phrases[index - 1].clone();
}
}
}
}
}
}
impl ArrangementTrack {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
clock: &Arc<TransportTime>,
name: &str,
color: Option<ItemColor>
) -> Usually<Self> {
Ok(Self {
name: Arc::new(RwLock::new(name.into())),
width: name.len() + 2,
color: color.unwrap_or_else(ItemColor::random),
player: PhrasePlayer::new(&jack, clock, name)?,
})
}
pub fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max)
}
pub const MIN_WIDTH: usize = 3;
pub fn width_inc (&mut self) { self.width += 1; }
pub fn width_dec (&mut self) { if self.width > Self::MIN_WIDTH { self.width -= 1; } }
}
/// Focus identification methods
impl ArrangementFocus {
pub fn description <E: Engine> (
&self,
tracks: &Vec<ArrangementTrack>,
scenes: &Vec<Scene>,
) -> String {
format!("Selected: {}", match self {
Self::Mix => format!("Everything"),
Self::Track(t) => match tracks.get(*t) {
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
None => format!("T??"),
},
Self::Scene(s) => match scenes.get(*s) {
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
None => format!("S??"),
},
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
(Some(_), Some(scene)) => match scene.clip(*t) {
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
None => format!("T{t} S{s}: Empty")
},
_ => format!("T{t} S{s}: Empty"),
}
})
}
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)) }
}
}
}
/// Arranger display mode can be cycled
impl ArrangementViewMode {
/// Cycle arranger display mode
pub fn to_next (&mut self) {
*self = match self {
Self::Horizontal => Self::Vertical(1),
Self::Vertical(1) => Self::Vertical(2),
Self::Vertical(2) => Self::Vertical(2),
Self::Vertical(0) => Self::Horizontal,
Self::Vertical(_) => Self::Vertical(0),
}
}
}
impl Scene {
pub fn new (
name: impl AsRef<str>,
clips: impl AsRef<[Option<Arc<RwLock<Phrase>>>]>,
color: Option<ItemColor>,
) -> Self {
Self {
name: Arc::new(RwLock::new(name.as_ref().into())),
clips: clips.as_ref().iter().map(|x|x.clone()).collect(),
color: color.unwrap_or_else(ItemColor::random),
}
}
/// Returns the pulse length of the longest phrase in the scene
pub fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all phrases in the scene are currently playing
pub fn is_playing (&self, tracks: &[ArrangementTrack]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(clip) => tracks
.get(track_index)
.map(|track|if let Some((_, Some(phrase))) = &track.player.phrase {
*phrase.read().unwrap() == *clip.read().unwrap()
} else {
false
})
.unwrap_or(false),
None => true
})
}
pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
let mut total = 0;
if factor == 0 {
scenes.iter().map(|scene|{
let pulses = scene.pulses().max(PPQ);
total = total + pulses;
(pulses, total - pulses)
}).collect()
} else {
(0..=scenes.len()).map(|i|{
(factor*PPQ, factor*PPQ*i)
}).collect()
}
}
pub fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max)
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}

View file

@ -0,0 +1,101 @@
use crate::*;
#[derive(Clone)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
Arrangement(ArrangementCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
#[derive(Clone)]
pub enum ArrangementCommand {
New,
Load,
Save,
ToggleViewMode,
Delete,
Activate,
Increment,
Decrement,
ZoomIn,
ZoomOut,
MoveBack,
MoveForward,
RandomColor,
Put,
Get,
AddScene,
AddTrack,
ToggleLoop,
GoUp,
GoDown,
GoLeft,
GoRight,
Edit(Option<Arc<RwLock<Phrase>>>),
}
impl<E: Engine> Command<Arranger<E>> for ArrangerCommand {
fn execute (self, state: &mut Arranger<E>) -> Perhaps<Self> {
let undo = match self {
Self::Focus(cmd) => {
delegate(cmd, Self::Focus, state)
},
Self::Phrases(cmd) => {
delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
},
Self::Editor(cmd) => {
delegate(cmd, Self::Editor, &mut state.editor)
},
Self::Arrangement(cmd) => {
delegate(cmd, Self::Arrangement, &mut state.arrangement)
},
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
} else {
Ok(None)
},
Self::EditPhrase(phrase) => {
state.editor.phrase = phrase.clone();
state.focus(ArrangerFocus::PhraseEditor);
state.focus_enter();
Ok(None)
}
}?;
state.show_phrase();
state.update_status();
return Ok(undo);
}
}
impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
fn execute (self, state: &mut Arrangement<E>) -> Perhaps<Self> {
use ArrangementCommand::*;
match self {
New => todo!(),
Load => todo!(),
Save => todo!(),
Edit(phrase) => { state.phrase = phrase.clone() },
ToggleViewMode => { state.mode.to_next(); },
Delete => { state.delete(); },
Activate => { state.activate(); },
Increment => { state.increment(); },
Decrement => { state.decrement(); },
ZoomIn => { state.zoom_in(); },
ZoomOut => { state.zoom_out(); },
MoveBack => { state.move_back(); },
MoveForward => { state.move_forward(); },
RandomColor => { state.randomize_color(); },
Put => { state.phrase_put(); },
Get => { state.phrase_get(); },
AddScene => { state.scene_add(None, None)?; },
AddTrack => { state.track_add(None, None)?; },
ToggleLoop => { state.toggle_loop() },
GoUp => { state.go_up() },
GoDown => { state.go_down() },
GoLeft => { state.go_left() },
GoRight => { state.go_right() },
};
Ok(None)
}
}

View file

@ -0,0 +1,54 @@
use crate::*;
/// Layout for standalone arranger app.
impl Content for Arranger<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.arrangement.focused;
let border_bg = Arranger::<Tui>::border_bg();
let border_fg = Arranger::<Tui>::border_fg(focused);
let title_fg = Arranger::<Tui>::title_fg(focused);
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
let entered = if self.arrangement.entered { "" } else { " " };
Split::down(
1,
row!(menu in self.menu.menus.iter() => {
row!(" ", menu.title.as_str(), " ")
}),
Split::up(
1,
widget(&self.status),
Split::up(
1,
widget(&self.transport),
Split::down(
self.arrangement_split,
lay!(
widget(&self.arrangement).grow_y(1).border(border),
widget(&self.arrangement.size),
widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1),
),
Split::right(
self.phrases_split,
self.phrases.clone(),
widget(&self.editor),
)
)
)
)
)
}
}
impl Content for Arrangement<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move |add|{
match self.mode {
ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) },
ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) },
}?;
add(&self.size)
})
}
}

View file

@ -0,0 +1,97 @@
use crate::*;
impl Content for ArrangerStatusBar {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let label = match self {
Self::Transport => "TRANSPORT",
Self::ArrangementMix => "PROJECT",
Self::ArrangementTrack => "TRACK",
Self::ArrangementScene => "SCENE",
Self::ArrangementClip => "CLIP",
Self::PhrasePool => "SEQ LIST",
Self::PhraseView => "VIEW SEQ",
Self::PhraseEdit => "EDIT SEQ",
};
let status_bar_bg = Arranger::<Tui>::status_bar_bg();
let mode_bg = Arranger::<Tui>::mode_bg();
let mode_fg = Arranger::<Tui>::mode_fg();
let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg);
let commands = match self {
Self::ArrangementMix => command(&[
["", "c", "olor"],
["", "<>", "resize"],
["", "+-", "zoom"],
["", "n", "ame/number"],
["", "Enter", " stop all"],
]),
Self::ArrangementClip => command(&[
["", "g", "et"],
["", "s", "et"],
["", "a", "dd"],
["", "i", "ns"],
["", "d", "up"],
["", "e", "dit"],
["", "c", "olor"],
["re", "n", "ame"],
["", ",.", "select"],
["", "Enter", " launch"],
]),
Self::ArrangementTrack => command(&[
["re", "n", "ame"],
["", ",.", "resize"],
["", "<>", "move"],
["", "i", "nput"],
["", "o", "utput"],
["", "m", "ute"],
["", "s", "olo"],
["", "Del", "ete"],
["", "Enter", " stop"],
]),
Self::ArrangementScene => command(&[
["re", "n", "ame"],
["", "Del", "ete"],
["", "Enter", " launch"],
]),
Self::PhrasePool => command(&[
["", "a", "ppend"],
["", "i", "nsert"],
["", "d", "uplicate"],
["", "Del", "ete"],
["", "c", "olor"],
["re", "n", "ame"],
["leng", "t", "h"],
["", ",.", "move"],
["", "+-", "resize view"],
]),
Self::PhraseView => command(&[
["", "enter", " edit"],
["", "arrows/pgup/pgdn", " scroll"],
["", "+=", "zoom"],
]),
Self::PhraseEdit => command(&[
["", "esc", " exit"],
["", "a", "ppend"],
["", "s", "et"],
["", "][", "length"],
["", "+-", "zoom"],
]),
_ => command(&[])
};
//let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
row!(mode, commands).fill_x().bg(status_bar_bg)
}
}
fn command (commands: &[[impl Widget<Engine = Tui>;3]]) -> impl Widget<Engine = Tui> + '_ {
Stack::right(|add|{
Ok(for [a, b, c] in commands.iter() {
add(&row!(
" ",
widget(a),
widget(b).bold(true).fg(Arranger::<Tui>::hotkey_fg()),
widget(c),
))?;
})
})
}

View file

@ -0,0 +1,161 @@
use crate::*;
/// Handle top-level events in standalone arranger.
impl Handle<Tui> for Arranger<Tui> {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
if let Some(entered) = self.entered() {
use ArrangerFocus::*;
if let Some(true) = match entered {
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
Arrangement => self.arrangement.handle(i)?,
PhrasePool => self.phrases.write().unwrap().handle(i)?,
PhraseEditor => self.editor.handle(i)?,
} {
return Ok(Some(true))
}
}
Ok(if let Some(command) = ArrangerCommand::input_to_command(self, i) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
}
}
impl InputToCommand<Tui, Arranger<Tui>> for ArrangerCommand {
fn input_to_command (state: &Arranger<Tui>, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
use ArrangerCommand::*;
match input.event() {
key!(KeyCode::Tab) => Some(Focus(Next)),
key!(Shift-KeyCode::Tab) => Some(Focus(Prev)),
key!(KeyCode::BackTab) => Some(Focus(Prev)),
key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)),
key!(KeyCode::Up) => Some(Focus(Up)),
key!(KeyCode::Down) => Some(Focus(Down)),
key!(KeyCode::Left) => Some(Focus(Left)),
key!(KeyCode::Right) => Some(Focus(Right)),
key!(KeyCode::Enter) => Some(Focus(Enter)),
key!(KeyCode::Esc) => Some(Focus(Exit)),
key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)),
_ => match state.focused() {
ArrangerFocus::Transport => state.transport.as_ref()
.map(|t|TransportCommand::input_to_command(&*t.read().unwrap(), input)
.map(Transport))
.flatten(),
ArrangerFocus::PhrasePool => {
let phrases = state.phrases.read().unwrap();
match input.event() {
key!(KeyCode::Char('e')) => Some(EditPhrase(Some(phrases.phrase().clone()))),
_ => PhrasePoolCommand::input_to_command(&*phrases, input)
.map(Phrases)
}
},
ArrangerFocus::PhraseEditor =>
PhraseEditorCommand::input_to_command(&state.editor, input)
.map(Editor),
ArrangerFocus::Arrangement => match input.event() {
key!(KeyCode::Char('e')) => Some(EditPhrase(state.arrangement.phrase())),
_ => ArrangementCommand::input_to_command(&state.arrangement, &input)
.map(Arrangement)
}
}
}
}
}
/// Handle events for arrangement.
impl Handle<Tui> for Arrangement<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
Ok(if let Some(command) = ArrangementCommand::input_to_command(self, from) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
}
}
impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
fn input_to_command (state: &Arrangement<Tui>, input: &TuiInput) -> Option<Self> {
use ArrangementCommand::*;
match input.event() {
key!(KeyCode::Char('`')) => Some(ToggleViewMode),
key!(KeyCode::Delete) => Some(Delete),
key!(KeyCode::Enter) => Some(Activate),
key!(KeyCode::Char('.')) => Some(Increment),
key!(KeyCode::Char(',')) => Some(Decrement),
key!(KeyCode::Char('+')) => Some(ZoomIn),
key!(KeyCode::Char('=')) => Some(ZoomOut),
key!(KeyCode::Char('_')) => Some(ZoomOut),
key!(KeyCode::Char('-')) => Some(ZoomOut),
key!(KeyCode::Char('<')) => Some(MoveBack),
key!(KeyCode::Char('>')) => Some(MoveForward),
key!(KeyCode::Char('c')) => Some(RandomColor),
key!(KeyCode::Char('s')) => Some(Put),
key!(KeyCode::Char('g')) => Some(Get),
key!(KeyCode::Char('e')) => Some(Edit(state.phrase())),
key!(Ctrl-KeyCode::Char('a')) => Some(AddScene),
key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack),
key!(KeyCode::Char('l')) => Some(ToggleLoop),
key!(KeyCode::Up) => Some(GoUp),
key!(KeyCode::Down) => Some(GoDown),
key!(KeyCode::Left) => Some(GoLeft),
key!(KeyCode::Right) => Some(GoRight),
_ => None
}
}
}
//impl Arranger<Tui> {
///// Helper for event passthru to focused component
//fn handle_focused (&mut self, from: &TuiInput) -> Perhaps<bool> {
//match self.focused() {
//ArrangerFocus::Transport => self.transport.handle(from),
//ArrangerFocus::PhrasePool => self.handle_pool(from),
//ArrangerFocus::PhraseEditor => self.editor.handle(from),
//ArrangerFocus::Arrangement => self.handle_arrangement(from)
//.and_then(|result|{self.show_phrase();Ok(result)}),
//}
//}
///// Helper for phrase event passthru when phrase pool is focused
//fn handle_pool (&mut self, from: &TuiInput) -> Perhaps<bool> {
//match from.event() {
//key!(KeyCode::Char('<')) => {
//self.phrases_split = self.phrases_split.saturating_sub(1).max(12);
//},
//key!(KeyCode::Char('>')) => {
//self.phrases_split = self.phrases_split + 1;
//},
//_ => return self.phrases.handle(from)
//}
//Ok(Some(true))
//}
///// Helper for phrase event passthru when arrangement is focused
//fn handle_arrangement (&mut self, from: &TuiInput) -> Perhaps<bool> {
//let mut handle_phrase = ||{
//let result = self.phrases.handle(from);
//self.arrangement.phrase_put();
//result
//};
//match from.event() {
//key!(KeyCode::Char('a')) => return handle_phrase(),
//key!(KeyCode::Char('i')) => return handle_phrase(),
//key!(KeyCode::Char('d')) => return handle_phrase(),
//key!(KeyCode::Char('<')) => if self.arrangement.selected == ArrangementFocus::Mix {
//self.arrangement_split = self.arrangement_split.saturating_sub(1).max(12);
//} else {
//return self.arrangement.handle(from)
//},
//key!(KeyCode::Char('>')) => if self.arrangement.selected == ArrangementFocus::Mix {
//self.arrangement_split = self.arrangement_split + 1;
//} else {
//return self.arrangement.handle(from)
//},
//_ => return self.arrangement.handle(from)
//}
//self.show_phrase();
//Ok(Some(true))
//}
//}

View file

@ -0,0 +1,39 @@
use crate::*;
pub trait ArrangerTheme<E: Engine> {
fn border_bg () -> Color;
fn border_fg (focused: bool) -> Color;
fn title_fg (focused: bool) -> Color;
fn separator_fg (focused: bool) -> Color;
fn hotkey_fg () -> Color;
fn mode_bg () -> Color;
fn mode_fg () -> Color;
fn status_bar_bg () -> Color;
}
impl ArrangerTheme<Tui> for Arranger<Tui> {
fn border_bg () -> Color {
Color::Rgb(40, 50, 30)
}
fn border_fg (focused: bool) -> Color {
if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }
}
fn title_fg (focused: bool) -> Color {
if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }
}
fn separator_fg (_: bool) -> Color {
Color::Rgb(0, 0, 0)
}
fn hotkey_fg () -> Color {
Color::Rgb(255, 255, 0)
}
fn mode_bg () -> Color {
Color::Rgb(150, 160, 90)
}
fn mode_fg () -> Color {
Color::Rgb(255, 255, 255)
}
fn status_bar_bg () -> Color {
Color::Rgb(28, 35, 25)
}
}

View file

@ -0,0 +1,197 @@
use crate::*;
impl<'a> Content for HorizontalArranger<'a, Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Arrangement { tracks, focused, .. } = self.0;
let _tracks = tracks.as_slice();
lay!(
focused.then_some(Background(Arranger::<Tui>::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 Arrangement { scenes, selected, .. } = self.0;
let area = to.area();
let mut x2 = 0;
let [x, y, _, height] = area;
Ok(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 = 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;
})
}),
)
)
}
}

View file

@ -0,0 +1,188 @@
use crate::*;
impl<'a> Content for VerticalArranger<'a, Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self(state, factor) = self;
let tracks = state.tracks.as_ref() as &[ArrangementTrack];
let scenes = state.scenes.as_ref();
let cols = state.track_widths();
let rows = Scene::ppqs(scenes, *factor);
let bg = state.color;
let clip_bg = Arranger::<Tui>::border_bg();
let sep_fg = Arranger::<Tui>::separator_fg(false);
let header_h = 3u16;//5u16;
let scenes_w = 3 + Scene::longest_name(scenes) as u16; // x of 1st track
let clock = &self.0.clock;
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 player = &track.player;
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))) = player.phrase.as_ref() {
let length = phrase.read().unwrap().length;
let elapsed = player.pulses_since_start().unwrap();
let elapsed = clock.timebase().format_beats_1_short(
(elapsed as usize % length) as f64
);
format!("▎+{elapsed:>}")
} else {
String::from("")
};
// beats until switchover
let until_next = player.next_phrase.as_ref().map(|(t, _)|{
let target = t.pulse.get();
let current = clock.current.pulse.get();
if target > current {
let remaining = target - current;
format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining))
} else {
String::new()
}
}).unwrap_or(String::from(""));
// name of active MIDI input
let input = format!("▎>{}", track.player.midi_inputs.get(0)
.map(|port|port.short_name())
.transpose()?
.unwrap_or("(none)".into()));
// name of active MIDI output
let output = format!("▎<{}", track.player.midi_outputs.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)
});
// scene titles
let scene_name = |scene, playing: bool, height|row!(
if playing { "" } else { " " },
TuiStyle::bold((scene as &Scene).name.read().unwrap().as_str(), true),
).fixed_xy(scenes_w, height);
// scene clips
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
let mut bg = clip_bg;
match (tracks.get(track), (scene as &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))?;
bg = color.dark.rgb;
if let Some((_, Some(ref playing))) = track.player.phrase {
if *playing.read().unwrap() == *phrase.read().unwrap() {
bg = color.light.rgb
}
};
},
_ => {}
};
add(&Background(bg))
}).fixed_xy(w, h);
// 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(&scene_name(scene, playing, height).bg(scene.color.rgb))?;
// clip per track:
Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() {
add(&scene_clip(scene, track, w as u16, height))?;
})
}).fixed_y(height)
}
).fixed_y((self.0.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 = state.focused;
let selected = state.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 {
ArrangementFocus::Mix => area,
ArrangementFocus::Track(t) => { track_area = Some(get_track_area(t)); area },
ArrangementFocus::Scene(s) => { scene_area = Some(get_scene_area(s)); area },
ArrangementFocus::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 = Arranger::<Tui>::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 = Arranger::<Tui>::title_fg(self.0.focused);
let size = format!("{}x{}", self.0.size.w(), self.0.size.h());
let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy();
lay!(arrangement, lower_right)
}
}

45
crates/tek_tui/src/lib.rs Normal file
View file

@ -0,0 +1,45 @@
pub(crate) use tek_core::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_core::jack::*;
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::sync::{Arc, Mutex, RwLock};
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
submod! {
arranger
arranger_cmd
arranger_snd
arranger_tui
arranger_tui_bar
arranger_tui_cmd
arranger_tui_col
arranger_tui_hor
arranger_tui_ver
sequencer
sequencer_cmd
sequencer_snd
sequencer_tui
transport
transport_cmd
transport_snd
transport_tui
mixer
mixer_snd
mixer_cmd
mixer_tui
sampler
sampler_snd
sampler_cmd
plugin
plugin_snd
plugin_cmd
plugin_tui
plugin_lv2
plugin_lv2_gui
plugin_vst2
plugin_vst3
}

133
crates/tek_tui/src/mixer.rs Normal file
View file

@ -0,0 +1,133 @@
use crate::*;
pub struct Mixer<E: Engine> {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub tracks: Vec<Track<E>>,
pub selected_track: usize,
pub selected_column: usize,
}
impl<E: Engine> Mixer<E> {
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
selected_column: 0,
selected_track: 1,
tracks: vec![],
})
}
pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> {
let track = Track::new(name)?;
self.tracks.push(track);
Ok(self)
}
pub fn track (&self) -> Option<&Track<E>> {
self.tracks.get(self.selected_track)
}
}
//pub const ACTIONS: [(&'static str, &'static str);2] = [
//("+/-", "Adjust"),
//("Ins/Del", "Add/remove track"),
//];
/// A sequencer track.
#[derive(Debug)]
pub struct Track<E: Engine> {
pub name: String,
/// Inputs and outputs of 1st and last device
pub ports: JackPorts,
/// Device chain
pub devices: Vec<JackDevice<E>>,
/// Device selector
pub device: usize,
}
impl<E: Engine> Track<E> {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
ports: JackPorts::default(),
devices: vec![],
device: 0,
})
}
fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
self.devices.get(i).map(|d|d.state.write().unwrap())
}
pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
self.get_device_mut(self.device)
}
/// Add a device to the end of the chain.
pub fn append_device (&mut self, device: JackDevice<E>) -> Usually<&mut JackDevice<E>> {
self.devices.push(device);
let index = self.devices.len() - 1;
Ok(&mut self.devices[index])
}
pub fn add_device (&mut self, device: JackDevice<E>) {
self.devices.push(device);
}
//pub fn connect_first_device (&self) -> Usually<()> {
//if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
//device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
//}
//Ok(())
//}
//pub fn connect_last_device (&self, app: &Track) -> Usually<()> {
//Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
//Some(device) => {
//app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
//app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
//()
//},
//None => ()
//})
//}
}
pub struct TrackView<'a, E: Engine> {
pub chain: Option<&'a Track<E>>,
pub direction: Direction,
pub focused: bool,
pub entered: bool,
}
impl<'a> Widget for TrackView<'a, Tui> {
type Engine = Tui;
fn layout (&self, area: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
todo!();
//let mut area = to.area();
//if let Some(chain) = self.chain {
//match self.direction {
//Direction::Down => area.width = area.width.min(40),
//Direction::Right => area.width = area.width.min(10),
//_ => { unimplemented!() },
//}
//to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered));
//let mut split = Stack::new(self.direction);
//for device in chain.devices.as_slice().iter() {
//split = split.add_ref(device);
//}
//let (area, areas) = split.render_areas(to)?;
//if self.focused && self.entered && areas.len() > 0 {
//Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?;
//}
//Ok(Some(area))
//} else {
//let [x, y, width, height] = area;
//let label = "No chain selected";
//let x = x + (width - label.len() as u16) / 2;
//let y = y + height / 2;
//to.blit(&label, x, y, Some(Style::default().dim().bold()))?;
//Ok(Some(area))
//}
}
}

View file

@ -0,0 +1,87 @@
use crate::*;
impl Handle<Tui> for Mixer<Tui> {
fn handle (&mut self, engine: &TuiInput) -> Perhaps<bool> {
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//self.exit();
//}
//},
KeyCode::Down => {
self.selected_track = (self.selected_track + 1) % self.tracks.len();
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Up => {
if self.selected_track == 0 {
self.selected_track = self.tracks.len() - 1;
} else {
self.selected_track -= 1;
}
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column -= 1;
}
return Ok(Some(true))
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column += 1;
}
return Ok(Some(true))
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(None)
}
}
impl Handle<Tui> for Track<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
//, NONE, "chain_cursor_up", "move cursor up", || {
key!(KeyCode::Up) => {
Ok(Some(true))
},
// , NONE, "chain_cursor_down", "move cursor down", || {
key!(KeyCode::Down) => {
Ok(Some(true))
},
// Left, NONE, "chain_cursor_left", "move cursor left", || {
key!(KeyCode::Left) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = track.device.saturating_sub(1);
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_cursor_right", "move cursor right", || {
key!(KeyCode::Right) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_mode_switch", "switch the display mode", || {
key!(KeyCode::Char('`')) => {
//app.chain_mode = !app.chain_mode;
Ok(Some(true))
},
_ => Ok(None)
}
}
}

View file

@ -0,0 +1,38 @@
use crate::*;
impl Content for Mixer<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::right(|add| {
for channel in self.tracks.iter() {
add(channel)?;
}
Ok(())
})
}
}
impl Content for Track<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
TrackView {
chain: Some(&self),
direction: tek_core::Direction::Right,
focused: true,
entered: true,
//pub channels: u8,
//pub input_ports: Vec<Port<AudioIn>>,
//pub pre_gain_meter: f64,
//pub gain: f64,
//pub insert_ports: Vec<Port<AudioOut>>,
//pub return_ports: Vec<Port<AudioIn>>,
//pub post_gain_meter: f64,
//pub post_insert_meter: f64,
//pub level: f64,
//pub pan: f64,
//pub output_ports: Vec<Port<AudioOut>>,
//pub post_fader_meter: f64,
//pub route: String,
}
}
}

View file

@ -0,0 +1,62 @@
use crate::*;
/// A plugin device.
pub struct Plugin<E> {
_engine: PhantomData<E>,
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub path: Option<String>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub ports: JackPorts,
}
impl<E> Plugin<E> {
/// Create a plugin host device.
pub fn new (
jack: &Arc<RwLock<JackClient>>,
name: &str,
) -> Usually<Self> {
Ok(Self {
_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
ports: JackPorts::default()
})
}
}
impl<E: Engine> Plugin<E> {
pub fn new_lv2 (
jack: &Arc<RwLock<JackClient>>,
name: &str,
path: &str,
) -> Usually<JackDevice<E>> {
let plugin = LV2Plugin::new(path)?;
jack_from_lv2(name, &plugin.plugin)?.run(|ports|Box::new(Self {
_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: Some(String::from(path)),
plugin: Some(PluginKind::LV2(plugin)),
selected: 0,
mapping: false,
ports
}))
}
}
/// Supported plugin formats.
pub enum PluginKind {
LV2(LV2Plugin),
VST2 {
instance: ::vst::host::PluginInstance
},
VST3,
}

View file

@ -0,0 +1,64 @@
use crate::*;
impl Handle<Tui> for Plugin<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
key!(KeyCode::Up) => {
self.selected = self.selected.saturating_sub(1);
Ok(Some(true))
},
key!(KeyCode::Down) => {
self.selected = (self.selected + 1).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key!(KeyCode::PageUp) => {
self.selected = self.selected.saturating_sub(8);
Ok(Some(true))
},
key!(KeyCode::PageDown) => {
self.selected = (self.selected + 10).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key!(KeyCode::Char(',')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value - 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key!(KeyCode::Char('.')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value + 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key!(KeyCode::Char('g')) => {
match self.plugin {
Some(PluginKind::LV2(ref mut plugin)) => {
plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
},
Some(_) => unreachable!(),
None => {}
}
Ok(Some(true))
},
_ => Ok(None)
}
}
}

View file

@ -0,0 +1,46 @@
use super::*;
use ::livi::{
World,
Instance,
Plugin as LiviPlugin,
Features,
FeaturesBuilder,
Port,
event::LV2AtomSequence,
};
use std::thread::JoinHandle;
/// A LV2 plugin.
pub struct LV2Plugin {
pub world: World,
pub instance: Instance,
pub plugin: LiviPlugin,
pub features: Arc<Features>,
pub port_list: Vec<Port>,
pub input_buffer: Vec<LV2AtomSequence>,
pub ui_thread: Option<JoinHandle<()>>,
}
impl LV2Plugin {
const INPUT_BUFFER: usize = 1024;
pub fn new (uri: &str) -> Usually<Self> {
// Get 1st plugin at URI
let world = World::with_load_bundle(&uri);
let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 };
let features = world.build_features(features);
let mut plugin = None;
if let Some(p) = world.iter_plugins().next() { plugin = Some(p); }
let plugin = plugin.expect("plugin not found");
let err = &format!("init {uri}");
let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) };
let mut port_list = vec![];
for port in plugin.ports() {
port_list.push(port);
}
let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER);
// Instantiate
Ok(Self {
world, instance, port_list, plugin, features, input_buffer, ui_thread: None
})
}
}

View file

@ -0,0 +1,76 @@
use crate::*;
use std::thread::{spawn, JoinHandle};
use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
//pub struct LV2PluginUI {
//write: (),
//controller: (),
//widget: (),
//features: (),
//transfer: (),
//}
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
Ok(spawn(move||{
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop.run_app(&mut ui).unwrap()
}))
}
/// A LV2 plugin's X11 UI.
pub struct LV2PluginUI {
pub window: Option<Window>
}
impl LV2PluginUI {
pub fn new () -> Usually<Self> {
Ok(Self { window: None })
}
}
impl ApplicationHandler for LV2PluginUI {
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
}
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
self.window.as_ref().unwrap().set_visible(false);
event_loop.exit();
},
WindowEvent::RedrawRequested => {
self.window.as_ref().unwrap().request_redraw();
}
_ => (),
}
}
}
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}
pub fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
let counts = plugin.port_counts();
let mut jack = Jack::new(name)?;
for i in 0..counts.atom_sequence_inputs {
jack = jack.midi_in(&format!("midi-in-{i}"))
}
for i in 0..counts.atom_sequence_outputs {
jack = jack.midi_out(&format!("midi-out-{i}"));
}
for i in 0..counts.audio_inputs {
jack = jack.audio_in(&format!("audio-in-{i}"));
}
for i in 0..counts.audio_outputs {
jack = jack.audio_out(&format!("audio-out-{i}"));
}
Ok(jack)
}

View file

@ -0,0 +1,53 @@
use crate::*;
impl Widget for Plugin<Tui> {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some(to))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
let area = to.area();
let [x, y, _, height] = area;
let mut width = 20u16;
match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
let end = start + height as usize - 2;
//draw_box(buf, Rect { x, y, width, height });
for i in start..end {
if let Some(port) = port_list.get(i) {
let value = if let Some(value) = instance.control_input(port.index) {
value
} else {
port.default_value
};
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
let label = &format!("{:25} = {value:.03}", port.name);
width = width.max(label.len() as u16 + 4);
let style = if i == self.selected {
Some(Style::default().green())
} else {
None
} ;
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
} else {
break
}
}
},
_ => {}
};
draw_header(self, to, x, y, width)?;
Ok(())
}
}
fn draw_header <E> (state: &Plugin<E>, to: &mut TuiOutput, x: u16, y: u16, w: u16) -> Usually<Rect> {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
Ok(Rect { x, y, width: w, height: 1 })
}

View file

@ -0,0 +1,13 @@
use crate::*;
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
fn set_vst_plugin <E: Engine> (host: &Arc<Mutex<Plugin<E>>>, _path: &str) -> Usually<PluginKind> {
let mut loader = ::vst::host::PluginLoader::load(
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
host.clone()
)?;
Ok(PluginKind::VST2 {
instance: loader.instance()?
})
}

View file

@ -0,0 +1 @@
//! TODO

View file

@ -0,0 +1,430 @@
use crate::*;
/// The sampler plugin plays sounds.
pub struct Sampler<E: Engine> {
_engine: PhantomData<E>,
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub cursor: (usize, usize),
pub editing: Option<Arc<RwLock<Sample>>>,
pub mapped: BTreeMap<u7, Arc<RwLock<Sample>>>,
pub unmapped: Vec<Arc<RwLock<Sample>>>,
pub voices: Arc<RwLock<Vec<Voice>>>,
pub ports: JackPorts,
pub buffer: Vec<Vec<f32>>,
pub modal: Arc<Mutex<Option<Box<dyn Exit + Send>>>>,
pub output_gain: f32
}
impl<E: Engine> Sampler<E> {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
name: &str,
mapped: Option<BTreeMap<u7, Arc<RwLock<Sample>>>>
) -> Usually<JackDevice<E>> {
Jack::new(name)?
.midi_in("midi")
.audio_in("recL")
.audio_in("recR")
.audio_out("outL")
.audio_out("outR")
.run(|ports|Box::new(Self {
_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
cursor: (0, 0),
editing: None,
mapped: mapped.unwrap_or_else(||BTreeMap::new()),
unmapped: vec![],
voices: Arc::new(RwLock::new(vec![])),
ports,
buffer: vec![vec![0.0;16384];2],
output_gain: 0.5,
modal: Default::default()
}))
}
/// Immutable reference to sample at cursor.
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
for (i, sample) in self.mapped.values().enumerate() {
if i == self.cursor.0 {
return Some(sample)
}
}
for (i, sample) in self.unmapped.iter().enumerate() {
if i + self.mapped.len() == self.cursor.0 {
return Some(sample)
}
}
None
}
/// Create [Voice]s from [Sample]s in response to MIDI input.
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
for RawMidi { time, bytes } in self.ports.midi_ins.get("midi").unwrap().iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
if let MidiMessage::NoteOn { ref key, ref vel } = message {
if let Some(sample) = self.mapped.get(key) {
self.voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
}
}
}
}
/// Zero the output buffer.
pub fn clear_output_buffer (&mut self) {
for buffer in self.buffer.iter_mut() {
buffer.fill(0.0);
}
}
/// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
let channel_count = self.buffer.len();
self.voices.write().unwrap().retain_mut(|voice|{
for index in 0..scope.n_frames() as usize {
if let Some(frame) = voice.next() {
for (channel, sample) in frame.iter().enumerate() {
// Averaging mixer:
//self.buffer[channel % channel_count][index] = (
//(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0
//);
self.buffer[channel % channel_count][index] +=
sample * self.output_gain;
}
} else {
return false
}
}
return true
});
}
/// Write output buffer to output ports.
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
for (i, port) in self.ports.audio_outs.values_mut().enumerate() {
let buffer = &self.buffer[i];
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
*value = *buffer.get(i).unwrap_or(&0.0);
}
}
}
}
/// A sound sample.
#[derive(Default, Debug)]
pub struct Sample {
pub name: String,
pub start: usize,
pub end: usize,
pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>,
}
impl Sample {
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self { name: name.to_string(), start, end, channels, rate: None }
}
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
}
/// Load sample from WAV and assign to MIDI note.
#[macro_export] macro_rules! sample {
($note:expr, $name:expr, $src:expr) => {{
let (end, data) = read_sample_data($src)?;
(
u7::from_int_lossy($note).into(),
Sample::new($name, 0, end, data).into()
)
}};
}
/// Read WAV from file
pub fn read_sample_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}
use std::fs::File;
use symphonia::core::codecs::CODEC_TYPE_NULL;
use symphonia::core::errors::Error;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::probe::Hint;
use symphonia::core::audio::SampleBuffer;
use symphonia::default::get_codecs;
pub struct AddSampleModal {
exited: bool,
dir: PathBuf,
subdirs: Vec<OsString>,
files: Vec<OsString>,
cursor: usize,
offset: usize,
sample: Arc<RwLock<Sample>>,
voices: Arc<RwLock<Vec<Voice>>>,
_search: Option<String>,
}
impl Exit for AddSampleModal {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
}
impl AddSampleModal {
pub fn new (
sample: &Arc<RwLock<Sample>>,
voices: &Arc<RwLock<Vec<Voice>>>
) -> Usually<Self> {
let dir = std::env::current_dir()?;
let (subdirs, files) = scan(&dir)?;
Ok(Self {
exited: false,
dir,
subdirs,
files,
cursor: 0,
offset: 0,
sample: sample.clone(),
voices: voices.clone(),
_search: None
})
}
fn rescan (&mut self) -> Usually<()> {
scan(&self.dir).map(|(subdirs, files)|{
self.subdirs = subdirs;
self.files = files;
})
}
fn prev (&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
fn next (&mut self) {
self.cursor = self.cursor + 1;
}
fn try_preview (&mut self) -> Usually<()> {
if let Some(path) = self.cursor_file() {
if let Ok(sample) = Sample::from_file(&path) {
*self.sample.write().unwrap() = sample;
self.voices.write().unwrap().push(
Sample::play(&self.sample, 0, &u7::from(100u8))
);
}
//load_sample(&path)?;
//let src = std::fs::File::open(&path)?;
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
//let mut hint = Hint::new();
//if let Some(ext) = path.extension() {
//hint.with_extension(&ext.to_string_lossy());
//}
//let meta_opts: MetadataOptions = Default::default();
//let fmt_opts: FormatOptions = Default::default();
//if let Ok(mut probed) = symphonia::default::get_probe()
//.format(&hint, mss, &fmt_opts, &meta_opts)
//{
//panic!("{:?}", probed.format.metadata());
//};
}
Ok(())
}
fn cursor_dir (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
return None
}
let index = self.cursor.saturating_sub(self.subdirs.len());
if index < self.files.len() {
Some(self.dir.join(&self.files[index]))
} else {
None
}
}
fn pick (&mut self) -> Usually<bool> {
if self.cursor == 0 {
if let Some(parent) = self.dir.parent() {
self.dir = parent.into();
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
}
if let Some(dir) = self.cursor_dir() {
self.dir = dir;
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
if let Some(path) = self.cursor_file() {
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
let mut sample = self.sample.write().unwrap();
sample.name = path.file_name().unwrap().to_string_lossy().into();
sample.end = end;
sample.channels = channels;
return Ok(true)
}
return Ok(false)
}
}
pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
modal.exit();
Ok(true)
}],
[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{
modal.prev();
Ok(true)
}],
[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{
modal.next();
Ok(true)
}],
[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{
if modal.pick()? {
modal.exit();
}
Ok(true)
}],
[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{
modal.try_preview()?;
Ok(true)
}]
});
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
let (mut subdirs, mut files) = read_dir(dir)?
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
let entry = entry.expect("failed to read drectory entry");
let meta = entry.metadata().expect("failed to read entry metadata");
if meta.is_file() {
files.push(entry.file_name());
} else if meta.is_dir() {
subdirs.push(entry.file_name());
}
(subdirs, files)
});
subdirs.sort();
files.sort();
Ok((subdirs, files))
}
impl Sample {
fn from_file (path: &PathBuf) -> Usually<Self> {
let mut sample = Self::default();
sample.name = path.file_name().unwrap().to_string_lossy().into();
// Use file extension if present
let mut hint = Hint::new();
if let Some(ext) = path.extension() {
hint.with_extension(&ext.to_string_lossy());
}
let probed = symphonia::default::get_probe().format(
&hint,
MediaSourceStream::new(
Box::new(File::open(path)?),
Default::default(),
),
&Default::default(),
&Default::default()
)?;
let mut format = probed.format;
let mut decoder = get_codecs().make(
&format.tracks().iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no tracks found")
.codec_params,
&Default::default()
)?;
loop {
match format.next_packet() {
Ok(packet) => {
// Decode a packet
let decoded = match decoder.decode(&packet) {
Ok(decoded) => decoded,
Err(err) => { return Err(err.into()); }
};
// Determine sample rate
let spec = *decoded.spec();
if let Some(rate) = sample.rate {
if rate != spec.rate as usize {
panic!("sample rate changed");
}
} else {
sample.rate = Some(spec.rate as usize);
}
// Determine channel count
while sample.channels.len() < spec.channels.count() {
sample.channels.push(vec![]);
}
// Load sample
let mut samples = SampleBuffer::new(
decoded.frames() as u64,
spec
);
if samples.capacity() > 0 {
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks(spec.channels.count()) {
for (chan, frame) in frame.iter().enumerate() {
sample.channels[chan].push(*frame)
}
}
}
},
Err(Error::IoError(_)) => break decoder.last_decoded(),
Err(err) => return Err(err.into()),
};
};
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample)
}
}
/// A currently playing instance of a sample.
pub struct Voice {
pub sample: Arc<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
if self.after > 0 {
self.after = self.after - 1;
return Some([0.0, 0.0])
}
let sample = self.sample.read().unwrap();
if self.position < sample.end {
let position = self.position;
self.position = self.position + 1;
return sample.channels[0].get(position).map(|_amplitude|[
sample.channels[0][position] * self.velocity,
sample.channels[0][position] * self.velocity,
])
}
None
}
}

View file

@ -0,0 +1,52 @@
use crate::*;
impl Handle<Tui> for Sampler<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
match from.event() {
key!(KeyCode::Up) => {
self.cursor.0 = if self.cursor.0 == 0 {
self.mapped.len() + self.unmapped.len() - 1
} else {
self.cursor.0 - 1
};
Ok(Some(true))
},
key!(KeyCode::Down) => {
self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len());
Ok(Some(true))
},
key!(KeyCode::Char('p')) => {
if let Some(sample) = self.sample() {
self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into()));
}
Ok(Some(true))
},
key!(KeyCode::Char('a')) => {
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?));
self.unmapped.push(sample);
Ok(Some(true))
},
key!(KeyCode::Char('r')) => {
if let Some(sample) = self.sample() {
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?));
}
Ok(Some(true))
},
key!(KeyCode::Enter) => {
if let Some(sample) = self.sample() {
self.editing = Some(sample.clone());
}
Ok(Some(true))
}
_ => Ok(None)
}
}
}
impl Handle<Tui> for AddSampleModal {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
return Ok(Some(true))
}
Ok(Some(true))
}
}

View file

@ -0,0 +1,98 @@
use crate::*;
impl Widget for Sampler<Tui> {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
tui_render_sampler(self, to)
}
}
pub fn tui_render_sampler (sampler: &Sampler<Tui>, to: &mut TuiOutput) -> Usually<()> {
let [x, y, _, height] = to.area();
let style = Style::default().gray();
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
let mut width = title.len() + 2;
let mut y1 = 1;
let mut j = 0;
for (note, sample) in sampler.mapped.iter()
.map(|(note, sample)|(Some(note), sample))
.chain(sampler.unmapped.iter().map(|sample|(None, sample)))
{
if y1 >= height {
break
}
let active = j == sampler.cursor.0;
width = width.max(
draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
);
y1 = y1 + 1;
j = j + 1;
}
let height = ((2 + y1) as u16).min(height);
//Ok(Some([x, y, (width as u16).min(to.area().w()), height]))
Ok(())
}
fn draw_sample (
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
impl Widget for AddSampleModal {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
//Align::Center(()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
todo!()
//let area = to.area();
//to.make_dim();
//let area = center_box(
//area,
//64.max(area.w().saturating_sub(8)),
//20.max(area.w().saturating_sub(8)),
//);
//to.fill_fg(area, Color::Reset);
//to.fill_bg(area, Nord::bg_lo(true, true));
//to.fill_char(area, ' ');
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
//for (i, (is_dir, name)) in self.subdirs.iter()
//.map(|path|(true, path))
//.chain(self.files.iter().map(|path|(false, path)))
//.enumerate()
//.skip(self.offset)
//{
//if i >= area.h() as usize - 4 {
//break
//}
//let t = if is_dir { "" } else { "" };
//let line = format!("{t} {}", name.to_string_lossy());
//let line = &line[..line.len().min(area.w() as usize - 4)];
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
//Style::default().green()
//} else {
//Style::default().white()
//}))?;
//}
//Lozenge(Style::default()).draw(to)
}
}

View file

@ -0,0 +1,481 @@
use crate::*;
use std::cmp::PartialEq;
/// MIDI message structural
pub type PhraseData = Vec<Vec<MidiMessage>>;
/// MIDI message serialized
pub type PhraseMessage = Vec<u8>;
/// Collection of serialized MIDI messages
pub type PhraseChunk = [Vec<PhraseMessage>];
/// Root level object for standalone `tek_sequencer`
pub struct Sequencer<E: Engine> {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
/// Controls the JACK transport.
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
/// Global timebase
pub clock: Arc<TransportTime>,
/// Pool of all phrases available to the sequencer
pub phrases: Arc<RwLock<PhrasePool<E>>>,
/// Phrase editor view
pub editor: PhraseEditor<E>,
/// Phrase player
pub player: PhrasePlayer,
/// Which view is focused
pub focus_cursor: (usize, usize),
/// Whether the currently focused item is entered
pub entered: bool,
}
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
/// Status bar for sequencer app
pub enum SequencerStatusBar {
Transport,
PhrasePool,
PhraseEditor,
}
/// Contains all phrases in a project
pub struct PhrasePool<E: Engine> {
_engine: PhantomData<E>,
/// Scroll offset
pub scroll: usize,
/// Highlighted phrase
pub phrase: usize,
/// Phrases in the pool
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Mode switch
pub mode: Option<PhrasePoolMode>,
/// Whether this widget is focused
pub focused: bool,
/// Whether this widget is entered
pub entered: bool,
}
/// Modes for phrase pool
pub enum PhrasePoolMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
}
/// A MIDI sequence.
#[derive(Debug, Clone)] pub struct Phrase {
pub uuid: uuid::Uuid,
/// Name of phrase
pub name: String,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of phrase in pulses
pub length: usize,
/// Notes in phrase
pub notes: PhraseData,
/// Whether to loop the phrase or play it once
pub loop_on: bool,
/// Start of loop
pub loop_start: usize,
/// Length of loop
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of phrase
pub color: ItemColorTriplet,
}
/// Contains state for viewing and editing a phrase
pub struct PhraseEditor<E: Engine> {
_engine: PhantomData<E>,
/// Phrase being played
pub phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub note_len: usize,
/// The full piano keys are rendered to this buffer
pub keys: Buffer,
/// The full piano roll is rendered to this buffer
pub buffer: BigBuffer,
/// Cursor/scroll/zoom in pitch axis
pub note_axis: RwLock<FixedAxis<usize>>,
/// Cursor/scroll/zoom in time axis
pub time_axis: RwLock<ScaledAxis<usize>>,
/// Whether this widget is focused
pub focused: bool,
/// Whether note enter mode is enabled
pub entered: bool,
/// Display mode
pub mode: bool,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub now: Arc<Pulse>,
/// Width of notes area at last render
pub width: AtomicUsize,
/// Height of notes area at last render
pub height: AtomicUsize,
}
/// Phrase player.
pub struct PhrasePlayer {
/// Global timebase
pub clock: Arc<TransportTime>,
/// Start time and phrase being played
pub phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
pub next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_inputs: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outputs: Vec<Port<MidiOut>>,
/// MIDI output buffer
pub midi_note: Vec<u8>,
/// MIDI output buffer
pub midi_chunk: Vec<Vec<Vec<u8>>>,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
}
/// Displays and edits phrase length.
pub struct PhraseLength<E: Engine> {
_engine: PhantomData<E>,
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of phrase in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<PhraseLengthFocus>,
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone)] pub enum PhraseLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
/// Focus layout of sequencer app
impl<E: Engine> FocusGrid for Sequencer<E> {
type Item = SequencerFocus;
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
fn layout (&self) -> &[&[SequencerFocus]] { &[
&[SequencerFocus::Transport],
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
] }
fn focus_enter (&mut self) { self.entered = true }
fn focus_exit (&mut self) { self.entered = false }
fn entered (&self) -> Option<Self::Item> {
if self.entered { Some(self.focused()) } else { None }
}
fn update_focus (&mut self) {
let focused = self.focused();
if let Some(transport) = self.transport.as_ref() {
transport.write().unwrap().focused = focused == SequencerFocus::Transport
}
self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool;
self.editor.focused = focused == SequencerFocus::PhraseEditor;
}
}
impl<E: Engine> PhrasePool<E> {
pub fn new () -> Self {
Self {
_engine: Default::default(),
scroll: 0,
phrase: 0,
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
mode: None,
focused: false,
entered: false,
}
}
pub fn len (&self) -> usize { self.phrases.len() }
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> { &self.phrases[self.phrase] }
pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) }
pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) }
pub fn index_before (&self, index: usize) -> usize {
index.overflowing_sub(1).0.min(self.len() - 1)
}
pub fn index_after (&self, index: usize) -> usize {
(index + 1) % self.len()
}
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
for i in 0..self.phrases.len() {
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
}
return None
}
fn new_phrase (name: Option<&str>, color: Option<ItemColorTriplet>) -> Arc<RwLock<Phrase>> {
Arc::new(RwLock::new(Phrase::new(
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
)))
}
pub fn delete_selected (&mut self) {
if self.phrase > 0 {
self.phrases.remove(self.phrase);
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
}
}
pub fn append_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.push(Self::new_phrase(name, color));
self.phrase = self.phrases.len() - 1;
}
pub fn insert_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
self.phrase += 1;
}
pub fn insert_dup (&mut self) {
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
self.phrase += 1;
}
pub fn randomize_color (&mut self) {
let mut phrase = self.phrases[self.phrase].write().unwrap();
phrase.color = ItemColorTriplet::random();
}
pub fn begin_rename (&mut self) {
self.mode = Some(PhrasePoolMode::Rename(
self.phrase,
self.phrases[self.phrase].read().unwrap().name.clone()
));
}
pub fn begin_length (&mut self) {
self.mode = Some(PhrasePoolMode::Length(
self.phrase,
self.phrases[self.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
));
}
pub fn move_up (&mut self) {
if self.phrase > 1 {
self.phrases.swap(self.phrase - 1, self.phrase);
self.phrase -= 1;
}
}
pub fn move_down (&mut self) {
if self.phrase < self.phrases.len().saturating_sub(1) {
self.phrases.swap(self.phrase + 1, self.phrase);
self.phrase += 1;
}
}
}
impl<E: Engine> PhraseEditor<E> {
pub fn new () -> Self {
Self {
_engine: Default::default(),
phrase: None,
note_len: 24,
notes_in: Arc::new(RwLock::new([false;128])),
notes_out: Arc::new(RwLock::new([false;128])),
keys: keys_vert(),
buffer: Default::default(),
focused: false,
entered: false,
mode: false,
now: Arc::new(0.into()),
width: 0.into(),
height: 0.into(),
note_axis: RwLock::new(FixedAxis {
start: 12,
point: Some(36),
clamp: Some(127)
}),
time_axis: RwLock::new(ScaledAxis {
start: 00,
point: Some(00),
clamp: Some(000),
scale: 24
}),
}
}
pub fn note_cursor_inc (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
}
pub fn note_cursor_dec (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
}
pub fn note_page_up (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
}
pub fn note_page_down (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
}
pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); }
pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); }
pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) }
pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) }
pub fn time_cursor_advance (&self) {
let point = self.time_axis.read().unwrap().point;
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length;
self.time_axis.write().unwrap().point = point.map(forward);
}
pub fn time_cursor_inc (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().point_inc(scale);
}
pub fn time_cursor_dec (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().point_dec(scale);
}
pub fn time_scroll_inc (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().start_inc(scale);
}
pub fn time_scroll_dec (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().start_dec(scale);
}
pub fn time_zoom_in (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().scale = prev_note_length(scale)
}
pub fn time_zoom_out (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().scale = next_note_length(scale)
}
}
impl Phrase {
pub fn new (
name: impl AsRef<str>,
loop_on: bool,
length: usize,
notes: Option<PhraseData>,
color: Option<ItemColorTriplet>,
) -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
name: name.as_ref().to_string(),
ppq: PPQ,
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
loop_on,
loop_start: 0,
loop_length: length,
percussive: true,
color: color.unwrap_or_else(ItemColorTriplet::random)
}
}
pub fn duplicate (&self) -> Self {
let mut clone = self.clone();
clone.uuid = uuid::Uuid::new_v4();
clone
}
pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; }
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() {
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
}
}
return false
}
}
impl Default for Phrase {
fn default () -> Self {
Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into()))
}
}
impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
impl Eq for Phrase {}
impl PhrasePlayer {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
clock: &Arc<TransportTime>,
name: &str
) -> Usually<Self> {
let jack = jack.read().unwrap();
Ok(Self {
clock: clock.clone(),
phrase: None,
next_phrase: None,
notes_in: Arc::new(RwLock::new([false;128])),
notes_out: Arc::new(RwLock::new([false;128])),
monitoring: false,
recording: false,
overdub: true,
reset: true,
midi_note: Vec::with_capacity(8),
midi_chunk: vec![Vec::with_capacity(16);16384],
midi_outputs: vec![
jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
],
midi_inputs: vec![
jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
],
})
}
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 enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
let start = self.clock.next_launch_pulse();
self.next_phrase = Some((
Instant::from_pulse(&self.clock.timebase(), start as f64),
phrase.map(|p|p.clone())
));
self.reset = true;
}
pub fn pulses_since_start (&self) -> Option<f64> {
if let Some((started, Some(_))) = self.phrase.as_ref() {
Some(self.clock.current.pulse.get() - started.pulse.get())
} else {
None
}
}
}
impl<E: Engine> PhraseLength<E> {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
}
impl PhraseLengthFocus {
pub fn next (&mut self) {
*self = match self {
Self::Bar => Self::Beat,
Self::Beat => Self::Tick,
Self::Tick => Self::Bar,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::Bar => Self::Tick,
Self::Beat => Self::Bar,
Self::Tick => Self::Beat,
}
}
}

View file

@ -0,0 +1,257 @@
use crate::*;
#[derive(Clone, PartialEq)]
pub enum SequencerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
}
#[derive(Clone, PartialEq)]
pub enum PhrasePoolCommand {
Prev,
Next,
MoveUp,
MoveDown,
Delete,
Append,
Insert,
Duplicate,
RandomColor,
Edit,
Import,
Export,
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
#[derive(Clone, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Backspace,
Append(char),
Set(String),
Confirm,
Cancel,
}
#[derive(Clone, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Next,
Prev,
Inc,
Dec,
Set(usize),
Confirm,
Cancel,
}
#[derive(Clone, PartialEq)]
pub enum PhraseEditorCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteCursorDec,
NoteCursorInc,
NoteLengthDec,
NoteLengthInc,
NotePageDown,
NotePageUp,
NoteScrollDec,
NoteScrollInc,
NoteSet,
TimeCursorDec,
TimeCursorInc,
TimeScrollDec,
TimeScrollInc,
TimeZoomIn,
TimeZoomOut,
GoUp,
GoDown,
GoLeft,
GoRight,
}
impl<E: Engine> Command<Sequencer<E>> for SequencerCommand {
fn execute (self, state: &mut Sequencer<E>) -> Perhaps<Self> {
match self {
Self::Focus(cmd) => {
return delegate(cmd, Self::Focus, state)
},
Self::Phrases(cmd) => {
return delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
},
Self::Editor(cmd) => {
return delegate(cmd, Self::Editor, &mut state.editor)
},
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
return delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
},
}
Ok(None)
}
}
impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand {
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
use PhrasePoolCommand::*;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self {
Rename(Rename::Begin) => { state.begin_rename() },
Length(Length::Begin) => { state.begin_length() },
Prev => { state.select_prev() },
Next => { state.select_next() },
Delete => { state.delete_selected() },
Append => { state.append_new(None, None) },
Insert => { state.insert_new(None, None) },
Duplicate => { state.insert_dup() },
RandomColor => { state.randomize_color() },
MoveUp => { state.move_up() },
MoveDown => { state.move_down() },
_ => unreachable!(),
}
Ok(None)
}
}
impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand {
fn translate (self, state: &PhrasePool<E>) -> Self {
use PhraseRenameCommand::*;
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode {
match self {
Backspace => {
let mut new_name = old_name.clone();
new_name.pop();
return Self::Set(new_name)
},
Append(c) => {
let mut new_name = old_name.clone();
new_name.push(c);
return Self::Set(new_name)
},
_ => {}
}
} else if self != Begin {
unreachable!()
}
self
}
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
use PhraseRenameCommand::*;
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode {
match self {
Set(s) => {
state.phrases[phrase].write().unwrap().name = s.into();
return Ok(Some(Self::Set(old_name.clone())))
},
Confirm => {
let old_name = old_name.clone();
state.mode = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
let mut phrase = state.phrases[phrase].write().unwrap();
phrase.name = old_name.clone();
},
_ => unreachable!()
};
Ok(None)
} else if self == Begin {
todo!()
} else {
unreachable!()
}
}
}
impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
fn translate (self, state: &PhrasePool<E>) -> Self {
use PhraseLengthCommand::*;
if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode {
match self {
Confirm => { return Self::Set(length) },
_ => self
}
} else if self == Begin {
todo!()
} else {
unreachable!()
}
}
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode {
match self {
Cancel => { state.mode = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let mut phrase = state.phrases[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
state.mode = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
}
Ok(None)
} else if self == Begin {
todo!()
} else {
unreachable!()
}
}
}
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
fn translate (self, state: &PhraseEditor<E>) -> Self {
use PhraseEditorCommand::*;
match self {
GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, },
GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, },
GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, },
GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, },
_ => self
}
}
fn execute (self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
use PhraseEditorCommand::*;
match self.translate(state) {
ToggleDirection => { state.mode = !state.mode; },
EnterEditMode => { state.entered = true; },
ExitEditMode => { state.entered = false; },
TimeZoomOut => { state.time_zoom_out() },
TimeZoomIn => { state.time_zoom_in() },
TimeCursorDec => { state.time_cursor_dec() },
TimeCursorInc => { state.time_cursor_inc() },
TimeScrollDec => { state.time_scroll_dec() },
TimeScrollInc => { state.time_scroll_inc() },
NoteCursorDec => { state.note_cursor_dec() },
NoteCursorInc => { state.note_cursor_inc() },
NoteScrollDec => { state.note_scroll_dec() },
NoteScrollInc => { state.note_scroll_inc() },
NoteLengthDec => { state.note_length_dec() },
NoteLengthInc => { state.note_length_inc() },
NotePageUp => { state.note_page_up() },
NotePageDown => { state.note_page_down() },
NoteAppend => {
if state.entered {
state.put();
state.time_cursor_advance();
}
},
NoteSet => {
if state.entered { state.put(); }
},
_ => unreachable!()
}
Ok(None)
}
}

View file

@ -0,0 +1,494 @@
use crate::*;
impl Content for Sequencer<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::down(move|add|{
add(&self.transport)?;
add(&self.phrases.clone()
.split(Direction::Right, 20, &self.editor as &dyn Widget<Engine = Tui>)
.min_y(20))
})
}
}
impl Handle<Tui> for Sequencer<Tui> {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
if let Some(entered) = self.entered() {
use SequencerFocus::*;
if let Some(true) = match entered {
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
PhrasePool => self.phrases.write().unwrap().handle(i)?,
PhraseEditor => self.editor.handle(i)?,
} {
return Ok(Some(true))
}
}
if let Some(command) = SequencerCommand::input_to_command(self, i) {
let _undo = command.execute(self)?;
return Ok(Some(true))
}
Ok(None)
}
}
impl InputToCommand<Tui, Sequencer<Tui>> for SequencerCommand {
fn input_to_command (state: &Sequencer<Tui>, input: &TuiInput) -> Option<Self> {
use SequencerCommand::*;
use FocusCommand::*;
match input.event() {
key!(KeyCode::Tab) => Some(Focus(Next)),
key!(Shift-KeyCode::Tab) => Some(Focus(Prev)),
key!(KeyCode::BackTab) => Some(Focus(Prev)),
key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)),
key!(KeyCode::Up) => Some(Focus(Up)),
key!(KeyCode::Down) => Some(Focus(Down)),
key!(KeyCode::Left) => Some(Focus(Left)),
key!(KeyCode::Right) => Some(Focus(Right)),
key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)),
_ => match state.focused() {
SequencerFocus::Transport => if let Some(t) = state.transport.as_ref() {
TransportCommand::input_to_command(&*t.read().unwrap(), input).map(Transport)
} else {
None
},
SequencerFocus::PhrasePool =>
PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input)
.map(Phrases),
SequencerFocus::PhraseEditor =>
PhraseEditorCommand::input_to_command(&state.editor, input)
.map(Editor),
}
}
}
}
// TODO: Display phrases always in order of appearance
impl Content for PhrasePool<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self { focused, phrases, mode, .. } = self;
let content = col!(
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
let mut length = PhraseLength::new(length, None);
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
if *focused && i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
}
let length = length.align_e().fill_x();
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
let mut row2 = format!(" {name}");
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
if *focused && i == *phrase { row2 = format!("{row2}"); }
};
let row2 = TuiStyle::bold(row2, true);
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
})
);
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
let upper_left = format!("[{}] Phrases", if self.entered {""} else {" "});
let upper_right = format!("({})", phrases.len());
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
)
}
}
impl Handle<Tui> for PhrasePool<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
if let Some(command) = PhrasePoolCommand::input_to_command(self, from) {
let _undo = command.execute(self)?;
return Ok(Some(true))
}
Ok(None)
}
}
impl InputToCommand<Tui, PhrasePool<Tui>> for PhrasePoolCommand {
fn input_to_command (state: &PhrasePool<Tui>, input: &TuiInput) -> Option<Self> {
match input.event() {
key!(KeyCode::Up) => Some(Self::Prev),
key!(KeyCode::Down) => Some(Self::Next),
key!(KeyCode::Char(',')) => Some(Self::MoveUp),
key!(KeyCode::Char('.')) => Some(Self::MoveDown),
key!(KeyCode::Delete) => Some(Self::Delete),
key!(KeyCode::Char('a')) => Some(Self::Append),
key!(KeyCode::Char('i')) => Some(Self::Insert),
key!(KeyCode::Char('d')) => Some(Self::Duplicate),
key!(KeyCode::Char('c')) => Some(Self::RandomColor),
key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)),
key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)),
_ => match state.mode {
Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input)
.map(Self::Rename),
Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input)
.map(Self::Length),
_ => None
}
}
}
}
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseRenameCommand {
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
match from.event() {
key!(KeyCode::Backspace) => Some(Self::Backspace),
key!(KeyCode::Enter) => Some(Self::Confirm),
key!(KeyCode::Esc) => Some(Self::Cancel),
key!(KeyCode::Char(c)) => Some(Self::Append(*c)),
_ => None
}
}
}
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseLengthCommand {
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
match from.event() {
key!(KeyCode::Up) => Some(Self::Inc),
key!(KeyCode::Down) => Some(Self::Dec),
key!(KeyCode::Right) => Some(Self::Next),
key!(KeyCode::Left) => Some(Self::Prev),
key!(KeyCode::Enter) => Some(Self::Confirm),
key!(KeyCode::Esc) => Some(Self::Cancel),
_ => None
}
}
}
impl Content for PhraseLength<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move|add|{
match self.focus {
None => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Bar) => add(&row!(
"[", self.bars_string(),
"]", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Beat) => add(&row!(
" ", self.bars_string(),
"[", self.beats_string(),
"]", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Tick) => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
"[", self.ticks_string(),
"]"
)),
}
})
}
}
impl Content for PhraseEditor<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self { focused, entered, keys, phrase, buffer, note_len, .. } = self;
let FixedAxis {
start: note_start, point: note_point, clamp: note_clamp
} = *self.note_axis.read().unwrap();
let ScaledAxis {
start: time_start, point: time_point, clamp: time_clamp, scale: time_scale
} = *self.time_axis.read().unwrap();
//let color = Color::Rgb(0,255,0);
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
Ok(if to.area().h() >= 2 {
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
let y = y + (note_start / 2) as u16;
if x < keys.area.width && y < keys.area.height {
*cell = keys.get(x, y).clone()
}
});
})
}).fill_y();
let notes_bg_null = Color::Rgb(28, 35, 25);
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
let area = to.area();
let h = area.h() as usize;
self.height.store(h, Ordering::Relaxed);
self.width.store(area.w() as usize, Ordering::Relaxed);
let mut axis = self.note_axis.write().unwrap();
if let Some(point) = axis.point {
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
axis.start += 2;
}
}
Ok(if to.area().h() >= 2 {
let area = to.area();
to.buffer_update(area, &move |cell, x, y|{
cell.set_bg(notes_bg_null);
let src_x = (x as usize + time_start) * time_scale;
let src_y = y as usize + note_start / 2;
if src_x < buffer.width && src_y < buffer.height - 1 {
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
cell.set_symbol(src.symbol());
cell.set_fg(src.fg);
cell.set_bg(src.bg);
});
}
});
})
}).fill_x();
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
Ok(if *focused && *entered {
let area = to.area();
if let (Some(time), Some(note)) = (time_point, note_point) {
let x1 = area.x() + (time / time_scale) as u16;
let x2 = x1 + (self.note_len / time_scale) as u16;
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
let c = if note % 2 == 0 { "" } else { "" };
for x in x1..x2 {
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
}
}
})
});
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
let playhead = CustomWidget::new(
|to:[u16;2]|Ok(Some(to.clip_h(1))),
move|to: &mut TuiOutput|{
if let Some(_) = phrase {
let now = self.now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
let time_clamp = time_clamp
.expect("time_axis of sequencer expected to be clamped");
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
let this_step = time_start + (x + 0) * time_scale;
let next_step = time_start + (x + 1) * time_scale;
let x = to.area().x() + x as u16;
let active = this_step <= now && now < next_step;
let character = if active { "|" } else { "·" };
let style = if active { playhead_active } else { playhead_inactive };
to.blit(&character, x, to.area.y(), Some(style));
}
}
Ok(())
}
).push_x(6).align_sw();
let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let note_area = lay!(notes, cursor).fill_x();
let piano_roll = row!(keys, note_area).fill_x();
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
let content = lay!(content, playhead);
let mut upper_left = format!("[{}] Sequencer", if *entered {""} else {" "});
if let Some(phrase) = phrase {
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
}
let mut lower_right = format!(
"┤{}x{}├",
self.width.load(Ordering::Relaxed),
self.height.load(Ordering::Relaxed),
);
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
//pulses_to_name(time_scale),
//time_start, time_point.unwrap_or(0),
//time_scale, time_clamp.unwrap_or(0),
//);
if *focused && *entered {
lower_right = format!("┤Note: {} {}├─{lower_right}",
self.note_axis.read().unwrap().point.unwrap(),
pulses_to_name(*note_len));
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
//pulses_to_name(*note_len),
//note_start,
//note_point.unwrap_or(0),
//note_clamp.unwrap_or(0),
//);
}
let upper_right = if let Some(phrase) = phrase {
format!("┤Length: {}", phrase.read().unwrap().length)
} else {
String::new()
};
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
)
}
}
impl<E: Engine> PhraseEditor<E> {
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);
}
}
}
}
}
/// 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
}
impl Handle<Tui> for PhraseEditor<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
if let Some(command) = PhraseEditorCommand::input_to_command(self, from) {
let _undo = command.execute(self)?;
return Ok(Some(true))
}
Ok(None)
}
}
impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
fn input_to_command (_: &PhraseEditor<Tui>, from: &TuiInput) -> Option<Self> {
match from.event() {
key!(KeyCode::Char('`')) => Some(Self::ToggleDirection),
key!(KeyCode::Enter) => Some(Self::EnterEditMode),
key!(KeyCode::Esc) => Some(Self::ExitEditMode),
key!(KeyCode::Char('[')) => Some(Self::NoteLengthDec),
key!(KeyCode::Char(']')) => Some(Self::NoteLengthInc),
key!(KeyCode::Char('a')) => Some(Self::NoteAppend),
key!(KeyCode::Char('s')) => Some(Self::NoteSet),
key!(KeyCode::Char('-')) => Some(Self::TimeZoomOut),
key!(KeyCode::Char('_')) => Some(Self::TimeZoomOut),
key!(KeyCode::Char('=')) => Some(Self::TimeZoomIn),
key!(KeyCode::Char('+')) => Some(Self::TimeZoomIn),
key!(KeyCode::PageUp) => Some(Self::NotePageUp),
key!(KeyCode::PageDown) => Some(Self::NotePageDown),
key!(KeyCode::Up) => Some(Self::GoUp),
key!(KeyCode::Down) => Some(Self::GoDown),
key!(KeyCode::Left) => Some(Self::GoLeft),
key!(KeyCode::Right) => Some(Self::GoRight),
_ => None
}
}
}

View file

@ -0,0 +1,6 @@
//! Multi-track mixer
include!("lib.rs");
pub fn main () -> Usually<()> {
Tui::run(Arc::new(RwLock::new(crate::Track::new("")?)))?;
Ok(())
}

View file

@ -0,0 +1,81 @@
use crate::*;
/// Stores and displays time-related state.
#[derive(Debug)]
pub struct TransportView<E: Engine> {
_engine: PhantomData<E>,
state: TransportToolbar,
focused: bool,
focus: TransportFocus,
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, PartialEq)]
pub enum TransportFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl<E: Engine> TransportView<E> {
pub fn new (jack: &Arc<RwLock<JackClient>>, clock: Option<&Arc<TransportTime>>) -> Self {
Self {
_engine: Default::default(),
focused: false,
focus: TransportFocus::PlayPause,
state: TransportToolbar {
metronome: false,
transport: jack.read().unwrap().transport(),
jack: jack.clone(),
clock: match clock {
Some(clock) => clock.clone(),
None => {
let timebase = Arc::new(Timebase::default());
Arc::new(TransportTime {
playing: Some(TransportState::Stopped).into(),
quant: 24.into(),
sync: (timebase.ppq.get() * 4.).into(),
current: Instant::default(),
started: None.into(),
})
}
},
}
}
}
pub fn toggle_play (&mut self) -> Usually<()> {
let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet");
let playing = match playing {
TransportState::Stopped => {
self.transport.start()?;
Some(TransportState::Starting)
},
_ => {
self.transport.stop()?;
self.transport.locate(0)?;
Some(TransportState::Stopped)
},
};
*self.clock.playing.write().unwrap() = playing;
Ok(())
}
}
impl TransportFocus {
pub fn next (&mut self) {
*self = match self {
Self::PlayPause => Self::Bpm,
Self::Bpm => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::Clock,
Self::Clock => Self::PlayPause,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::PlayPause => Self::Clock,
Self::Bpm => Self::PlayPause,
Self::Quant => Self::Bpm,
Self::Sync => Self::Quant,
Self::Clock => Self::Sync,
}
}
}

View file

@ -0,0 +1,103 @@
use crate::*;
#[derive(Copy, Clone, PartialEq)]
pub enum TransportCommand {
FocusNext,
FocusPrev,
Play,
Pause,
PlayToggle,
PlayFromStart,
Increment,
Decrement,
FineIncrement,
FineDecrement,
SeekUsec(f64),
SeekSample(f64),
SeekPulse(f64),
SetBpm(f64),
SetQuant(f64),
SetSync(f64),
}
impl<E: Engine> Command<TransportView<E>> for TransportCommand {
fn translate (self, state: &TransportView<E>) -> Self {
use TransportCommand::*;
use TransportViewFocus::*;
match self {
Increment => match state.focus {
Bpm =>
{return SetBpm(state.clock.timebase().bpm.get() + 1.0) },
Quant =>
{return SetQuant(next_note_length(state.clock.quant.get()as usize)as f64)},
Sync =>
{return SetSync(next_note_length(state.clock.sync.get()as usize)as f64+1.)},
PlayPause =>
{/*todo seek*/},
Clock =>
{/*todo seek*/},
},
FineIncrement => match state.focus {
Bpm =>
{return SetBpm(state.clock.timebase().bpm.get() + 0.001)},
Quant =>
{return Increment},
Sync =>
{return Increment},
PlayPause =>
{/*todo seek*/},
Clock =>
{/*todo seek*/},
},
Decrement => match state.focus {
Bpm =>
{return SetBpm(state.clock.timebase().bpm.get() - 1.0)},
Quant =>
{return SetQuant(prev_note_length(state.clock.quant.get()as usize)as f64)},
Sync =>
{return SetSync(prev_note_length(state.clock.sync.get()as usize)as f64)},
PlayPause =>
{/*todo seek*/},
Clock =>
{/*todo seek*/},
},
FineDecrement => match state.focus {
Bpm =>
{return SetBpm(state.clock.timebase().bpm.get() - 0.001)},
Quant =>
{return Decrement},
Sync =>
{return Decrement},
PlayPause =>
{/*todo seek*/},
Clock =>
{/*todo seek*/},
},
_ => {}
};
return self
}
fn execute (self, state: &mut TransportView<E>) -> Perhaps<Self> {
use TransportCommand::*;
match self.translate(&state) {
FocusNext =>
{ state.focus.next(); },
FocusPrev =>
{ state.focus.prev(); },
PlayToggle =>
{ state.toggle_play()?; },
SeekUsec(usec) =>
{ state.clock.current.update_from_usec(usec); },
SeekSample(sample) =>
{ state.clock.current.update_from_sample(sample); },
SeekPulse(pulse) =>
{ state.clock.current.update_from_pulse(pulse); },
SetBpm(bpm) =>
{ return Ok(Some(Self::SetBpm(state.clock.timebase().bpm.set(bpm)))) },
SetQuant(quant) =>
{ return Ok(Some(Self::SetQuant(state.clock.quant.set(quant)))) },
SetSync(sync) =>
{ return Ok(Some(Self::SetSync(state.clock.sync.set(sync)))) },
_ => { unreachable!() }
}
Ok(None)
}
}

View file

@ -0,0 +1,71 @@
use crate::*;
impl Content for TransportToolbar<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
lay!(
self.focus.wrap(self.focused, TransportToolbarFocus::PlayPause, &Styled(
None,
match *self.clock.playing.read().unwrap() {
Some(TransportState::Rolling) => "▶ PLAYING",
Some(TransportState::Starting) => "READY ...",
Some(TransportState::Stopped) => "⏹ STOPPED",
_ => unreachable!(),
}
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
row!(
self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, {
let bpm = self.clock.timebase().bpm.get();
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
})),
//let quant = self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, row! {
//"QUANT ", ppq_to_name(self.quant as usize)
//})),
self.focus.wrap(self.focused, TransportToolbarFocus::Sync, &Outset::X(1u16, row! {
"SYNC ", pulses_to_name(self.clock.sync.get() as usize)
}))
).align_w().fill_x(),
self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{
let time1 = self.clock.current.format_beat();
let time2 = self.clock.current.usec.format_msu();
row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1)
}).align_e().fill_x(),
).fill_x().bg(Color::Rgb(40, 50, 30))
}
}
impl TransportToolbarFocus {
pub fn wrap <'a, W: Widget<Engine = Tui>> (
self, parent_focus: bool, focus: Self, widget: &'a W
) -> impl Widget<Engine = Tui> + 'a {
let focused = parent_focus && focus == self;
let corners = focused.then_some(CORNERS);
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!(corners, highlight, *widget)
}
}
impl Handle<Tui> for TransportToolbar<Tui> {
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
if let Some(command) = TransportCommand::input_to_command(self, from) {
let _undo = command.execute(self)?;
return Ok(Some(true))
}
Ok(None)
}
}
impl InputToCommand<Tui, TransportToolbar<Tui>> for TransportCommand {
fn input_to_command (_: &TransportToolbar<Tui>, input: &TuiInput) -> Option<Self> {
match input.event() {
key!(KeyCode::Char(' ')) => Some(Self::FocusPrev),
key!(Shift-KeyCode::Char(' ')) => Some(Self::FocusPrev),
key!(KeyCode::Left) => Some(Self::FocusPrev),
key!(KeyCode::Right) => Some(Self::FocusNext),
key!(KeyCode::Char('.')) => Some(Self::Increment),
key!(KeyCode::Char(',')) => Some(Self::Decrement),
key!(KeyCode::Char('>')) => Some(Self::FineIncrement),
key!(KeyCode::Char('<')) => Some(Self::FineDecrement),
_ => None
}
}
}