wip: p.52, e=5, collecting tui by layer p.2

This commit is contained in:
🪞👃🪞 2024-11-17 17:29:46 +01:00
parent 7b3c013aa7
commit 9b996878c2
31 changed files with 2005 additions and 2030 deletions

View file

@ -13,42 +13,28 @@ use std::fmt::Debug;
submod! {
tui_model
tui_jack
tui_handle
tui_apis
tui_cmd
tui_focus
tui_status
tui_handle
tui_jack
tui_menu
tui_model
tui_status
tui_theme
tui_select
tui_arranger
tui_arranger_cmd
tui_arranger_scene
tui_arranger_select
tui_arranger_track
tui_arranger_view
tui_phrase
tui_sequencer
//tui_mixer // TODO
tui_phrase
tui_phrase_cmd
tui_phrase_view
//tui_plugin // TODO
//tui_plugin_lv2
//tui_plugin_lv2_gui
//tui_plugin_vst2
//tui_plugin_vst3
tui_pool
tui_pool_cmd
tui_pool_view
//tui_sampler // TODO
//tui_sampler_cmd
tui_sequencer
tui_sequencer_cmd
tui_sequencer_view
tui_theme
tui_transport
tui_transport_cmd
tui_transport_view
}
// TODO

View file

@ -0,0 +1,566 @@
use crate::*;
impl HasPhrases for ArrangerTui {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases
}
}
impl HasPhrases for SequencerTui {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases
}
}
impl HasPhrase for SequencerTui {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn phrase_mut (&self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
}
impl ArrangerSceneApi for ArrangerScene {
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
&self.clips
}
fn color (&self) -> ItemColor {
self.color
}
}
impl HasScenes<ArrangerScene> for ArrangerTui {
fn scenes (&self) -> &Vec<ArrangerScene> {
&self.scenes
}
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
&mut self.scenes
}
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerScene>
{
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
let scene = ArrangerScene {
name: Arc::new(name.into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(||ItemColor::random()),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok(&mut self.scenes_mut()[index])
}
}
impl HasTracks<ArrangerTrack> for ArrangerTui {
fn tracks (&self) -> &Vec<ArrangerTrack> {
&self.tracks
}
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
&mut self.tracks
}
}
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerTrack>
{
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
let track = ArrangerTrack {
width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(||ItemColor::random()),
midi_ins: vec![],
midi_outs: vec![],
reset: true,
recording: false,
monitoring: false,
overdub: false,
play_phrase: None,
next_phrase: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
};
self.tracks_mut().push(track);
let index = self.tracks().len() - 1;
Ok(&mut self.tracks_mut()[index])
}
fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
}
impl ArrangerTrackApi for ArrangerTrack {
/// Name of track
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
/// Preferred width of track column
fn width (&self) -> usize {
self.width
}
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize {
&mut self.width
}
/// Identifying color of track
fn color (&self) -> ItemColor {
self.color
}
}
impl HasPhrase for ArrangerTrack {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn phrase_mut (&self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
}
impl MidiInputApi for ArrangerTrack {
fn midi_ins (&self) -> &Vec<Port<jack::MidiIn>> {
todo!()
}
fn midi_ins_mut (&self) -> &mut Vec<Port<jack::MidiIn>> {
todo!()
}
fn recording (&self) -> bool {
todo!()
}
fn recording_mut (&mut self) -> &mut bool {
todo!()
}
fn monitoring (&self) -> bool {
todo!()
}
fn monitoring_mut (&mut self) -> &mut bool {
todo!()
}
fn overdub (&self) -> bool {
todo!()
}
fn overdub_mut (&mut self) -> &mut bool {
todo!()
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
todo!()
}
}
impl MidiOutputApi for ArrangerTrack {
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
todo!()
}
fn midi_outs_mut (&mut self) -> &mut Vec<Port<jack::MidiOut>> {
todo!()
}
fn midi_note (&mut self) -> &mut Vec<u8> {
todo!()
}
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
todo!()
}
}
impl ClockApi for ArrangerTrack {
fn timebase (&self) -> &Arc<Timebase> {
todo!()
}
fn quant (&self) -> &Quantize {
todo!()
}
fn sync (&self) -> &LaunchSync {
todo!()
}
}
impl PlayheadApi for ArrangerTrack {
fn current (&self) -> &Instant {
todo!()
}
fn transport (&self) -> &Transport {
todo!()
}
fn playing (&self) -> &RwLock<Option<TransportState>> {
todo!()
}
fn started (&self) -> &RwLock<Option<(usize, usize)>> {
todo!()
}
}
impl PlayerApi for ArrangerTrack {}
/// General methods for arranger
impl ArrangerTui {
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
}
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
}
pub fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
/// Focus the editor with the current phrase
pub fn show_phrase (&mut self) {
self.editor.show(self.selected_phrase().as_ref());
}
pub fn activate (&mut self) {
let scenes = self.scenes();
let tracks = self.tracks_mut();
match self.selected {
ArrangerSelection::Scene(s) => {
for (t, track) in tracks.iter_mut().enumerate() {
let player = &mut track.player;
let clip = 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()
//}
},
ArrangerSelection::Clip(t, s) => {
tracks[t].player.enqueue_next(scenes[s].clips[t]);
},
_ => {}
}
}
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 {
ArrangerSelection::Scene(s) => s == self.scenes().len() - 1,
ArrangerSelection::Clip(_, s) => s == self.scenes().len() - 1,
_ => false
}
}
pub fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
pub fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => {
self.color = ItemColor::random_dark()
},
ArrangerSelection::Track(t) => {
self.tracks_mut()[t].color = ItemColor::random()
},
ArrangerSelection::Scene(s) => {
self.scenes_mut()[s].color = ItemColor::random()
},
ArrangerSelection::Clip(t, s) => {
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
phrase.write().unwrap().color = ItemColorTriplet::random();
}
}
}
}
}
impl PhrasesTui {
pub fn new (phrases: Vec<Arc<RwLock<Phrase>>>) -> Self {
Self {
scroll: 0,
phrase: 0,
mode: None,
focused: false,
entered: false,
phrases,
}
}
pub fn len (&self) -> usize {
self.phrases.len()
}
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> {
&self.phrases[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 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;
}
}
}
//pub fn track_next (&mut self, last_track: usize) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Track(0),
//Track(t) => Track(last_track.min(*t + 1)),
//Scene(s) => Clip(0, *s),
//Clip(t, s) => Clip(last_track.min(*t + 1), *s),
//}
//}
//pub fn track_prev (&mut self) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Mix,
//Scene(s) => Scene(*s),
//Track(t) => if *t == 0 { Mix } else { Track(*t - 1) },
//Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) }
//}
//}
//pub fn scene_next (&mut self, last_scene: usize) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Scene(0),
//Track(t) => Clip(*t, 0),
//Scene(s) => Scene(last_scene.min(*s + 1)),
//Clip(t, s) => Clip(*t, last_scene.min(*s + 1)),
//}
//}
//pub fn scene_prev (&mut self) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Mix,
//Track(t) => Track(*t),
//Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) },
//Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) }
//}
//}
//pub fn arranger_menu_bar () -> MenuBar {
//use ArrangerCommand as Cmd;
//use ArrangerCommand as Edit;
//use ArrangerSelection as Focus;
//use ArrangerTrackCommand as Track;
//use ArrangerClipCommand as Clip;
//use ArrangerSceneCommand as Scene;
//use TransportCommand as Transport;
//MenuBar::new()
//.add({
//use ArrangerCommand::*;
//Menu::new("File")
//.cmd("n", "New project", ArrangerViewCommand::Arranger(New))
//.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load))
//.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save))
//})
//.add({
//Menu::new("Transport")
//.cmd("p", "Play", TransportCommand::Transport(Play(None)))
//.cmd("P", "Play from start", TransportCommand::Transport(Play(Some(0))))
//.cmd("s", "Pause", TransportCommand::Transport(Stop(None)))
//.cmd("S", "Stop and rewind", TransportCommand::Transport(Stop(Some(0))))
//})
//.add({
//use ArrangerCommand::*;
//Menu::new("Track")
//.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack))
//.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack))
//})
//.add({
//use ArrangerCommand::*;
//Menu::new("Scene")
//.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene))
//.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack))
//.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack))
//})
//.add({
//use PhraseRenameCommand as Rename;
//use PhraseLengthCommand as Length;
//Menu::new("Phrase")
//.cmd("a", "Append new", PhrasePoolCommand::Phrases(Append))
//.cmd("i", "Insert new", PhrasePoolCommand::Phrases(Insert))
//.cmd("n", "Rename", PhrasePoolCommand::Phrases(Rename(Rename::Begin)))
//.cmd("t", "Set length", PhrasePoolCommand::Phrases(Length(Length::Begin)))
//.cmd("d", "Delete", PhrasePoolCommand::Phrases(Delete))
//.cmd("l", "Load from MIDI...", PhrasePoolCommand::Phrases(Import))
//.cmd("s", "Save to MIDI...", PhrasePoolCommand::Phrases(Export))
//.cmd(">", "Move up", PhrasePoolCommand::Phrases(MoveUp))
//.cmd("<", "Move down", PhrasePoolCommand::Phrases(MoveDown))
//})
//}
//pub fn phrase_next (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] {
//let phrases = self.model.phrases.read().unwrap();
//let index = phrases.index_of(&*phrase.read().unwrap());
//if let Some(index) = index {
//if index < phrases.len().saturating_sub(1) {
//*phrase = phrases[index + 1].clone();
//}
//}
//}
//}
//}
//pub fn phrase_prev (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] {
//let phrases = self.model.phrases.read().unwrap();
//let index = phrases.index_of(&*phrase.read().unwrap());
//if let Some(index) = index {
//if index > 0 {
//*phrase = phrases[index - 1].clone();
//}
//}
//}
//}
//}
//pub fn phrase_get (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//if let Some(phrase) = &self.model.scenes[scene].clips[track] {
//let mut phrases = self.model.phrases.write().unwrap();
//if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) {
//self.model.phrase = index;
//}
//}
//}
//}
///// 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.append_new(None, Some(self.next_color().into()));
//self.arrangement.phrase_put();
//}
//self.show_phrase();
//self.focus(ArrangerFocus::PhraseEditor);
//self.editor.entered = true;
//}
//pub fn next_color (&self) -> ItemColor {
//if let ArrangerSelection::Clip(track, scene) = self.arrangement.selected {
//let track_color = self.arrangement.model.tracks[track].color;
//let scene_color = self.arrangement.model.scenes[scene].color;
//track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25)
//} else {
//panic!("could not compute next color")
//}
//}
//pub fn phrase_del (&mut self) {
//let track_index = self.selected.track();
//let scene_index = self.selected.scene();
//track_index
//.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track)))
//.map(|(track_index, _)|scene_index
//.and_then(|index|self.model.scenes.get_mut(index))
//.map(|scene|scene.clips[track_index] = None));
//}
//pub fn phrase_put (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//self.model.scenes[scene].clips[track] = self.selected_phrase().clone();
//}
//}
//pub fn selected_scene (&self) -> Option<&ArrangerScene> {
//self.selected.scene().map(|s|self.model.scenes.get(s)).flatten()
//}
//pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
//self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten()
//}
//pub fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
//self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
//}

View file

@ -1,278 +1 @@
use crate::*;
impl HasPhrases for ArrangerTui {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases
}
}
/// General methods for arranger
impl ArrangerTui {
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
}
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
}
pub fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
/// Focus the editor with the current phrase
pub fn show_phrase (&mut self) {
self.editor.show(self.selected_phrase().as_ref());
}
pub fn activate (&mut self) {
let scenes = self.scenes();
let tracks = self.tracks_mut();
match self.selected {
ArrangerSelection::Scene(s) => {
for (t, track) in tracks.iter_mut().enumerate() {
let player = &mut track.player;
let clip = 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()
//}
},
ArrangerSelection::Clip(t, s) => {
tracks[t].player.enqueue_next(scenes[s].clips[t]);
},
_ => {}
}
}
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 {
ArrangerSelection::Scene(s) => s == self.scenes().len() - 1,
ArrangerSelection::Clip(_, s) => s == self.scenes().len() - 1,
_ => false
}
}
pub fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
pub fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => {
self.color = ItemColor::random_dark()
},
ArrangerSelection::Track(t) => {
self.tracks_mut()[t].color = ItemColor::random()
},
ArrangerSelection::Scene(s) => {
self.scenes_mut()[s].color = ItemColor::random()
},
ArrangerSelection::Clip(t, s) => {
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
phrase.write().unwrap().color = ItemColorTriplet::random();
}
}
}
}
}
//pub fn track_next (&mut self, last_track: usize) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Track(0),
//Track(t) => Track(last_track.min(*t + 1)),
//Scene(s) => Clip(0, *s),
//Clip(t, s) => Clip(last_track.min(*t + 1), *s),
//}
//}
//pub fn track_prev (&mut self) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Mix,
//Scene(s) => Scene(*s),
//Track(t) => if *t == 0 { Mix } else { Track(*t - 1) },
//Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) }
//}
//}
//pub fn scene_next (&mut self, last_scene: usize) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Scene(0),
//Track(t) => Clip(*t, 0),
//Scene(s) => Scene(last_scene.min(*s + 1)),
//Clip(t, s) => Clip(*t, last_scene.min(*s + 1)),
//}
//}
//pub fn scene_prev (&mut self) {
//use ArrangerSelection::*;
//*self = match self {
//Mix => Mix,
//Track(t) => Track(*t),
//Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) },
//Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) }
//}
//}
//pub fn arranger_menu_bar () -> MenuBar {
//use ArrangerCommand as Cmd;
//use ArrangerCommand as Edit;
//use ArrangerSelection as Focus;
//use ArrangerTrackCommand as Track;
//use ArrangerClipCommand as Clip;
//use ArrangerSceneCommand as Scene;
//use TransportCommand as Transport;
//MenuBar::new()
//.add({
//use ArrangerCommand::*;
//Menu::new("File")
//.cmd("n", "New project", ArrangerViewCommand::Arranger(New))
//.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load))
//.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save))
//})
//.add({
//Menu::new("Transport")
//.cmd("p", "Play", TransportCommand::Transport(Play(None)))
//.cmd("P", "Play from start", TransportCommand::Transport(Play(Some(0))))
//.cmd("s", "Pause", TransportCommand::Transport(Stop(None)))
//.cmd("S", "Stop and rewind", TransportCommand::Transport(Stop(Some(0))))
//})
//.add({
//use ArrangerCommand::*;
//Menu::new("Track")
//.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack))
//.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack))
//})
//.add({
//use ArrangerCommand::*;
//Menu::new("Scene")
//.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene))
//.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack))
//.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack))
//.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack))
//})
//.add({
//use PhraseRenameCommand as Rename;
//use PhraseLengthCommand as Length;
//Menu::new("Phrase")
//.cmd("a", "Append new", PhrasePoolCommand::Phrases(Append))
//.cmd("i", "Insert new", PhrasePoolCommand::Phrases(Insert))
//.cmd("n", "Rename", PhrasePoolCommand::Phrases(Rename(Rename::Begin)))
//.cmd("t", "Set length", PhrasePoolCommand::Phrases(Length(Length::Begin)))
//.cmd("d", "Delete", PhrasePoolCommand::Phrases(Delete))
//.cmd("l", "Load from MIDI...", PhrasePoolCommand::Phrases(Import))
//.cmd("s", "Save to MIDI...", PhrasePoolCommand::Phrases(Export))
//.cmd(">", "Move up", PhrasePoolCommand::Phrases(MoveUp))
//.cmd("<", "Move down", PhrasePoolCommand::Phrases(MoveDown))
//})
//}
//pub fn phrase_next (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] {
//let phrases = self.model.phrases.read().unwrap();
//let index = phrases.index_of(&*phrase.read().unwrap());
//if let Some(index) = index {
//if index < phrases.len().saturating_sub(1) {
//*phrase = phrases[index + 1].clone();
//}
//}
//}
//}
//}
//pub fn phrase_prev (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] {
//let phrases = self.model.phrases.read().unwrap();
//let index = phrases.index_of(&*phrase.read().unwrap());
//if let Some(index) = index {
//if index > 0 {
//*phrase = phrases[index - 1].clone();
//}
//}
//}
//}
//}
//pub fn phrase_get (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//if let Some(phrase) = &self.model.scenes[scene].clips[track] {
//let mut phrases = self.model.phrases.write().unwrap();
//if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) {
//self.model.phrase = index;
//}
//}
//}
//}
///// 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.append_new(None, Some(self.next_color().into()));
//self.arrangement.phrase_put();
//}
//self.show_phrase();
//self.focus(ArrangerFocus::PhraseEditor);
//self.editor.entered = true;
//}
//pub fn next_color (&self) -> ItemColor {
//if let ArrangerSelection::Clip(track, scene) = self.arrangement.selected {
//let track_color = self.arrangement.model.tracks[track].color;
//let scene_color = self.arrangement.model.scenes[scene].color;
//track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25)
//} else {
//panic!("could not compute next color")
//}
//}
//pub fn phrase_del (&mut self) {
//let track_index = self.selected.track();
//let scene_index = self.selected.scene();
//track_index
//.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track)))
//.map(|(track_index, _)|scene_index
//.and_then(|index|self.model.scenes.get_mut(index))
//.map(|scene|scene.clips[track_index] = None));
//}
//pub fn phrase_put (&mut self) {
//if let ArrangerSelection::Clip(track, scene) = self.selected {
//self.model.scenes[scene].clips[track] = self.selected_phrase().clone();
//}
//}
//pub fn selected_scene (&self) -> Option<&ArrangerScene> {
//self.selected.scene().map(|s|self.model.scenes.get(s)).flatten()
//}
//pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
//self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten()
//}
//pub fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
//self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
//}

View file

@ -1,207 +0,0 @@
use crate::*;
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Clock(ClockCommand),
Playhead(PlayheadCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasePoolCommand),
Editor(PhraseCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
pub trait ArrangerControl {
}
impl ArrangerControl for ArrangerTui {
}
impl<T: ArrangerControl> InputToCommand<Tui, T> for ArrangerCommand {
fn input_to_command (view: &T, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
use ArrangerCommand::*;
Some(match input.event() {
key!(KeyCode::Tab) => Self::Focus(Next),
key!(Shift-KeyCode::Tab) => Self::Focus(Prev),
key!(KeyCode::BackTab) => Self::Focus(Prev),
key!(Shift-KeyCode::BackTab) => Self::Focus(Prev),
key!(KeyCode::Up) => Self::Focus(Up),
key!(KeyCode::Down) => Self::Focus(Down),
key!(KeyCode::Left) => Self::Focus(Left),
key!(KeyCode::Right) => Self::Focus(Right),
key!(KeyCode::Enter) => Self::Focus(Enter),
key!(KeyCode::Esc) => Self::Focus(Exit),
key!(KeyCode::Char(' ')) => {
Self::App(Playhead(PlayheadCommand::Play(None)))
},
_ => Self::App(match view.focused() {
ArrangerFocus::Transport => {
use TransportCommand::{Clock, Playhead};
match TransportCommand::input_to_command(view, input)? {
Clock(command) => {
todo!()
},
Playhead(command) => {
todo!()
},
}
},
ArrangerFocus::PhraseEditor => Editor(
PhraseCommand::input_to_command(&view.editor, input)?
),
ArrangerFocus::PhrasePool => match input.event() {
key!(KeyCode::Char('e')) => EditPhrase(
Some(view.phrase().clone())
),
_ => Phrases(
PhrasePoolCommand::input_to_command(view, input)?
)
},
ArrangerFocus::Arranger => {
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
use ArrangerSceneCommand as Scene;
match input.event() {
key!(KeyCode::Char('e')) => EditPhrase(view.phrase()),
_ => match input.event() {
// FIXME: boundary conditions
key!(KeyCode::Up) => match view.selected {
Select::Mix => return None,
Select::Track(t) => return None,
Select::Scene(s) => Select(Select::Scene(s - 1)),
Select::Clip(t, s) => Select(Select::Clip(t, s - 1)),
},
key!(KeyCode::Down) => match view.selected {
Select::Mix => Select(Select::Scene(0)),
Select::Track(t) => Select(Select::Clip(t, 0)),
Select::Scene(s) => Select(Select::Scene(s + 1)),
Select::Clip(t, s) => Select(Select::Clip(t, s + 1)),
},
key!(KeyCode::Left) => match view.selected {
Select::Mix => return None,
Select::Track(t) => Select(Select::Track(t - 1)),
Select::Scene(s) => return None,
Select::Clip(t, s) => Select(Select::Clip(t - 1, s)),
},
key!(KeyCode::Right) => match view.selected {
Select::Mix => return None,
Select::Track(t) => Select(Select::Track(t + 1)),
Select::Scene(s) => Select(Select::Clip(0, s)),
Select::Clip(t, s) => Select(Select::Clip(t, s - 1)),
},
key!(KeyCode::Char('+')) => Zoom(0),
key!(KeyCode::Char('=')) => Zoom(0),
key!(KeyCode::Char('_')) => Zoom(0),
key!(KeyCode::Char('-')) => Zoom(0),
key!(KeyCode::Char('`')) => { todo!("toggle view mode") },
key!(KeyCode::Char(',')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t - 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('.')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t + 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('<')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t - 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('>')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t + 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Enter) => match view.selected {
Select::Mix => return None,
Select::Track(t) => return None,
Select::Scene(s) => Scene(Scene::Play(s)),
Select::Clip(t, s) => return None,
},
key!(KeyCode::Delete) => match view.selected {
Select::Mix => Clear,
Select::Track(t) => Track(Track::Delete(t)),
Select::Scene(s) => Scene(Scene::Delete(s)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('c')) => Clip(Clip::RandomColor),
key!(KeyCode::Char('s')) => match view.selected {
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
_ => return None,
},
key!(KeyCode::Char('g')) => match view.selected {
Select::Clip(t, s) => Clip(Clip::Get(t, s)),
_ => return None,
},
key!(Ctrl-KeyCode::Char('a')) => Scene(Scene::Add),
key!(Ctrl-KeyCode::Char('t')) => Track(Track::Add),
key!(KeyCode::Char('l')) => Clip(Clip::SetLoop(false)),
_ => return None
}
}
}
})
})
}
}
impl<T: ArrangerControl> Command<T> for ArrangerCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use ArrangerCommand::*;
match self {
Focus(cmd) => { delegate(cmd, Focus, state) },
Scene(cmd) => { delegate(cmd, Scene, &mut state) },
Track(cmd) => { delegate(cmd, Track, &mut state) },
Clip(cmd) => { delegate(cmd, Clip, &mut state) },
Phrases(cmd) => { delegate(cmd, Phrases, &mut state) },
Editor(cmd) => { delegate(cmd, Editor, &mut state) },
Clock(cmd) => { delegate(cmd, Clock, &mut state) },
Playhead(cmd) => { delegate(cmd, Playhead, &mut state) },
Zoom(zoom) => { todo!(); },
Select(selected) => { state.selected = selected; Ok(None) },
EditPhrase(phrase) => {
state.editor.phrase = phrase.clone();
state.focus(ArrangerFocus::PhraseEditor);
state.focus_enter();
Ok(None)
}
}
}
}

View file

@ -1,45 +0,0 @@
use crate::*;
impl HasScenes<ArrangerScene> for ArrangerTui {
fn scenes (&self) -> &Vec<ArrangerScene> {
&self.scenes
}
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
&mut self.scenes
}
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerScene>
{
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
let scene = ArrangerScene {
name: Arc::new(name.into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(||ItemColor::random()),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok(&mut self.scenes_mut()[index])
}
}
#[derive(Default, Debug, Clone)]
pub struct ArrangerScene {
/// 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,
}
impl ArrangerSceneApi for ArrangerScene {
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
&self.clips
}
fn color (&self) -> ItemColor {
self.color
}
}

View file

@ -1,191 +0,0 @@
use crate::*;
impl HasTracks<ArrangerTrack> for ArrangerTui {
fn tracks (&self) -> &Vec<ArrangerTrack> {
&self.tracks
}
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
&mut self.tracks
}
}
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerTrack>
{
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
let track = ArrangerTrack {
width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(||ItemColor::random()),
midi_ins: vec![],
midi_outs: vec![],
reset: true,
recording: false,
monitoring: false,
overdub: false,
play_phrase: None,
next_phrase: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
};
self.tracks_mut().push(track);
let index = self.tracks().len() - 1;
Ok(&mut self.tracks_mut()[index])
}
fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
}
#[derive(Debug)]
pub struct ArrangerTrack {
/// Name of track
name: Arc<RwLock<String>>,
/// Preferred width of track column
width: usize,
/// Identifying color of track
color: ItemColor,
/// Start time and phrase being played
play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
monitoring: bool,
/// Write input to sequence.
recording: bool,
/// Overdub input to sequence.
overdub: bool,
/// Send all notes off
reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
midi_ins: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
midi_outs: Vec<Port<MidiOut>>,
/// Notes currently held at input
notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
notes_out: Arc<RwLock<[bool; 128]>>,
///// MIDI output buffer
//midi_note: Vec<u8>,
///// MIDI output buffer
//midi_chunk: Vec<Vec<Vec<u8>>>,
}
impl ArrangerTrackApi for ArrangerTrack {
/// Name of track
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
/// Preferred width of track column
fn width (&self) -> usize {
self.width
}
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize {
&mut self.width
}
/// Identifying color of track
fn color (&self) -> ItemColor {
self.color
}
}
impl HasPhrase for ArrangerTrack {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn phrase_mut (&self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
}
impl MidiInputApi for ArrangerTrack {
fn midi_ins (&self) -> &Vec<Port<jack::MidiIn>> {
todo!()
}
fn midi_ins_mut (&self) -> &mut Vec<Port<jack::MidiIn>> {
todo!()
}
fn recording (&self) -> bool {
todo!()
}
fn recording_mut (&mut self) -> &mut bool {
todo!()
}
fn monitoring (&self) -> bool {
todo!()
}
fn monitoring_mut (&mut self) -> &mut bool {
todo!()
}
fn overdub (&self) -> bool {
todo!()
}
fn overdub_mut (&mut self) -> &mut bool {
todo!()
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
todo!()
}
}
impl MidiOutputApi for ArrangerTrack {
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
todo!()
}
fn midi_outs_mut (&mut self) -> &mut Vec<Port<jack::MidiOut>> {
todo!()
}
fn midi_note (&mut self) -> &mut Vec<u8> {
todo!()
}
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
todo!()
}
}
impl ClockApi for ArrangerTrack {
fn timebase (&self) -> &Arc<Timebase> {
todo!()
}
fn quant (&self) -> &Quantize {
todo!()
}
fn sync (&self) -> &LaunchSync {
todo!()
}
}
impl PlayheadApi for ArrangerTrack {
fn current (&self) -> &Instant {
todo!()
}
fn transport (&self) -> &Transport {
todo!()
}
fn playing (&self) -> &RwLock<Option<TransportState>> {
todo!()
}
fn started (&self) -> &RwLock<Option<(usize, usize)>> {
todo!()
}
}
impl PlayerApi for ArrangerTrack {}

View file

@ -0,0 +1,738 @@
use crate::*;
#[derive(Clone, Debug, PartialEq)]
pub enum TransportCommand {
Focus(FocusCommand),
Clock(ClockCommand),
Playhead(PlayheadCommand),
}
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
use KeyCode::Char;
use ClockCommand::{SetBpm, SetQuant, SetSync};
use TransportFocus as Focused;
use TransportCommand::{Focus, Clock, Playhead};
let focused = state.focused();
Some(match input.event() {
key!(Left) => Focus(FocusCommand::Prev),
key!(Right) => Focus(FocusCommand::Next),
key!(Char('.')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() + 1.0)),
Focused::Quant => Clock(SetQuant(state.next_quant())),
Focused::Sync => Clock(SetSync(state.next_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
key!(KeyCode::Char(',')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() - 1.0)),
Focused::Quant => Clock(SetQuant(state.prev_quant())),
Focused::Sync => Clock(SetSync(state.prev_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
key!(KeyCode::Char('>')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() + 0.001)),
Focused::Quant => Clock(SetQuant(state.next_quant())),
Focused::Sync => Clock(SetSync(state.next_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
key!(KeyCode::Char('<')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() - 0.001)),
Focused::Quant => Clock(SetQuant(state.prev_quant())),
Focused::Sync => Clock(SetSync(state.prev_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
_ => return None
})
}
}
pub trait TransportControl {
fn quant (&self) -> &Quantize;
fn bpm (&self) -> &BeatsPerMinute;
fn next_quant (&self) -> f64 {
next_note_length(self.quant().get() as usize) as f64
}
fn prev_quant (&self) -> f64 {
prev_note_length(self.quant().get() as usize) as f64
}
fn sync (&self) -> &LaunchSync;
fn next_sync (&self) -> f64 {
next_note_length(self.sync().get() as usize) as f64
}
fn prev_sync (&self) -> f64 {
prev_note_length(self.sync().get() as usize) as f64
}
}
impl TransportControl for TransportTui {
fn bpm (&self) -> &BeatsPerMinute {
self.bpm()
}
fn quant (&self) -> &Quantize {
self.quant()
}
fn sync (&self) -> &LaunchSync {
self.sync()
}
}
impl<T: TransportControl> Command<T> for TransportCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use TransportCommand::{Focus, Clock, Playhead};
use ClockCommand::{SetBpm, SetQuant, SetSync};
Ok(Some(match self {
Focus(Next) => { todo!() }
Focus(Prev) => { todo!() },
Focus(_) => { unimplemented!() },
Clock(SetBpm(bpm)) => Clock(SetBpm(state.bpm().set(bpm))),
Clock(SetQuant(quant)) => Clock(SetQuant(state.quant().set(quant))),
Clock(SetSync(sync)) => Clock(SetSync(state.sync().set(sync))),
_ => return Ok(None)
}))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SequencerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Clock(ClockCommand),
Playhead(PlayheadCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
use SequencerCommand::*;
match input.event() {
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)),
key!(KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(KeyCode::Up) => Some(Self::Focus(Up)),
key!(KeyCode::Down) => Some(Self::Focus(Down)),
key!(KeyCode::Left) => Some(Self::Focus(Left)),
key!(KeyCode::Right) => Some(Self::Focus(Right)),
_ => Some(Self::App(match state.focused() {
SequencerFocus::Transport =>
TransportCommand::input_to_command(&state, input).map(Transport),
SequencerFocus::Phrases =>
PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases),
SequencerFocus::PhraseEditor =>
PhraseCommand::input_to_command(&state.editor, input).map(Editor),
_ => return None,
}))
}
}
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
use SequencerCommand::*;
match self {
Focus(cmd) => delegate(cmd, Focus, state),
Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases),
Editor(cmd) => delegate(cmd, Editor, &mut state.editor),
Clock(cmd) => delegate(cmd, Clock, &mut state.transport),
Playhead(cmd) => delegate(cmd, Playhead, &mut state.transport)
}
}
}
impl TransportControl for SequencerTui {
fn bpm (&self) -> &BeatsPerMinute {
self.app.bpm()
}
fn quant (&self) -> &Quantize {
self.app.quant()
}
fn sync (&self) -> &LaunchSync {
self.app.sync()
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Clock(ClockCommand),
Playhead(PlayheadCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasePoolCommand),
Editor(PhraseCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
pub trait ArrangerControl {
}
impl ArrangerControl for ArrangerTui {
}
impl<T: ArrangerControl> InputToCommand<Tui, T> for ArrangerCommand {
fn input_to_command (view: &T, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
use ArrangerCommand::*;
Some(match input.event() {
key!(KeyCode::Tab) => Self::Focus(Next),
key!(Shift-KeyCode::Tab) => Self::Focus(Prev),
key!(KeyCode::BackTab) => Self::Focus(Prev),
key!(Shift-KeyCode::BackTab) => Self::Focus(Prev),
key!(KeyCode::Up) => Self::Focus(Up),
key!(KeyCode::Down) => Self::Focus(Down),
key!(KeyCode::Left) => Self::Focus(Left),
key!(KeyCode::Right) => Self::Focus(Right),
key!(KeyCode::Enter) => Self::Focus(Enter),
key!(KeyCode::Esc) => Self::Focus(Exit),
key!(KeyCode::Char(' ')) => {
Self::App(Playhead(PlayheadCommand::Play(None)))
},
_ => Self::App(match view.focused() {
ArrangerFocus::Transport => {
use TransportCommand::{Clock, Playhead};
match TransportCommand::input_to_command(view, input)? {
Clock(command) => {
todo!()
},
Playhead(command) => {
todo!()
},
}
},
ArrangerFocus::PhraseEditor => Editor(
PhraseCommand::input_to_command(&view.editor, input)?
),
ArrangerFocus::PhrasePool => match input.event() {
key!(KeyCode::Char('e')) => EditPhrase(
Some(view.phrase().clone())
),
_ => Phrases(
PhrasePoolCommand::input_to_command(view, input)?
)
},
ArrangerFocus::Arranger => {
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
use ArrangerSceneCommand as Scene;
match input.event() {
key!(KeyCode::Char('e')) => EditPhrase(view.phrase()),
_ => match input.event() {
// FIXME: boundary conditions
key!(KeyCode::Up) => match view.selected {
Select::Mix => return None,
Select::Track(t) => return None,
Select::Scene(s) => Select(Select::Scene(s - 1)),
Select::Clip(t, s) => Select(Select::Clip(t, s - 1)),
},
key!(KeyCode::Down) => match view.selected {
Select::Mix => Select(Select::Scene(0)),
Select::Track(t) => Select(Select::Clip(t, 0)),
Select::Scene(s) => Select(Select::Scene(s + 1)),
Select::Clip(t, s) => Select(Select::Clip(t, s + 1)),
},
key!(KeyCode::Left) => match view.selected {
Select::Mix => return None,
Select::Track(t) => Select(Select::Track(t - 1)),
Select::Scene(s) => return None,
Select::Clip(t, s) => Select(Select::Clip(t - 1, s)),
},
key!(KeyCode::Right) => match view.selected {
Select::Mix => return None,
Select::Track(t) => Select(Select::Track(t + 1)),
Select::Scene(s) => Select(Select::Clip(0, s)),
Select::Clip(t, s) => Select(Select::Clip(t, s - 1)),
},
key!(KeyCode::Char('+')) => Zoom(0),
key!(KeyCode::Char('=')) => Zoom(0),
key!(KeyCode::Char('_')) => Zoom(0),
key!(KeyCode::Char('-')) => Zoom(0),
key!(KeyCode::Char('`')) => { todo!("toggle view mode") },
key!(KeyCode::Char(',')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t - 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('.')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t + 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('<')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t - 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s - 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('>')) => match view.selected {
Select::Mix => Zoom(0),
Select::Track(t) => Track(Track::Swap(t, t + 1)),
Select::Scene(s) => Scene(Scene::Swap(s, s + 1)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Enter) => match view.selected {
Select::Mix => return None,
Select::Track(t) => return None,
Select::Scene(s) => Scene(Scene::Play(s)),
Select::Clip(t, s) => return None,
},
key!(KeyCode::Delete) => match view.selected {
Select::Mix => Clear,
Select::Track(t) => Track(Track::Delete(t)),
Select::Scene(s) => Scene(Scene::Delete(s)),
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
},
key!(KeyCode::Char('c')) => Clip(Clip::RandomColor),
key!(KeyCode::Char('s')) => match view.selected {
Select::Clip(t, s) => Clip(Clip::Set(t, s, None)),
_ => return None,
},
key!(KeyCode::Char('g')) => match view.selected {
Select::Clip(t, s) => Clip(Clip::Get(t, s)),
_ => return None,
},
key!(Ctrl-KeyCode::Char('a')) => Scene(Scene::Add),
key!(Ctrl-KeyCode::Char('t')) => Track(Track::Add),
key!(KeyCode::Char('l')) => Clip(Clip::SetLoop(false)),
_ => return None
}
}
}
})
})
}
}
impl<T: ArrangerControl> Command<T> for ArrangerCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use ArrangerCommand::*;
match self {
Focus(cmd) => { delegate(cmd, Focus, state) },
Scene(cmd) => { delegate(cmd, Scene, &mut state) },
Track(cmd) => { delegate(cmd, Track, &mut state) },
Clip(cmd) => { delegate(cmd, Clip, &mut state) },
Phrases(cmd) => { delegate(cmd, Phrases, &mut state) },
Editor(cmd) => { delegate(cmd, Editor, &mut state) },
Clock(cmd) => { delegate(cmd, Clock, &mut state) },
Playhead(cmd) => { delegate(cmd, Playhead, &mut state) },
Zoom(zoom) => { todo!(); },
Select(selected) => { state.selected = selected; Ok(None) },
EditPhrase(phrase) => {
state.editor.phrase = phrase.clone();
state.focus(ArrangerFocus::PhraseEditor);
state.focus_enter();
Ok(None)
}
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
Select(usize),
Edit(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Set(String),
Confirm,
Cancel,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Next,
Prev,
Inc,
Dec,
Set(usize),
Cancel,
}
impl InputToCommand<Tui, PhrasesTui> for PhrasesCommand {
fn input_to_command (state: &PhrasesTui, input: &TuiInput) -> Option<Self> {
use PhrasesCommand as Cmd;
use PhrasePoolCommand as Edit;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match input.event() {
key!(KeyCode::Up) => Some(Cmd::Select(0)),
key!(KeyCode::Down) => Some(Cmd::Select(0)),
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))),
key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))),
key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))),
key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))),
key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))),
key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)),
key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)),
_ => match state.mode {
Some(PhrasesMode::Rename(..)) => {
Rename::input_to_command(state, input).map(Cmd::Rename)
},
Some(PhrasesMode::Length(..)) => {
Length::input_to_command(state, input).map(Cmd::Length)
},
_ => None
}
}
}
}
impl Command<PhrasesTui> for PhrasesCommand {
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self {
Self::Select(phrase) => {
view.phrase = phrase
},
Self::Edit(command) => {
return Ok(command.execute(&mut view)?.map(Self::Edit))
}
Self::Rename(command) => match command {
Rename::Begin => {
view.mode = Some(PhrasesMode::Rename(
view.phrase,
view.phrases[view.phrase].read().unwrap().name.clone()
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Rename))
}
},
Self::Length(command) => match command {
Length::Begin => {
view.mode = Some(PhrasesMode::Length(
view.phrase,
view.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Length))
}
},
}
Ok(None)
}
}
impl InputToCommand<Tui, PhrasesTui> for PhraseLengthCommand {
fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option<Self> {
if let Some(PhrasesMode::Length(_, length, _)) = view.mode {
Some(match from.event() {
key!(KeyCode::Up) => Self::Inc,
key!(KeyCode::Down) => Self::Dec,
key!(KeyCode::Right) => Self::Next,
key!(KeyCode::Left) => Self::Prev,
key!(KeyCode::Enter) => Self::Set(length),
key!(KeyCode::Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl Command<PhrasesTui> for PhraseLengthCommand {
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = view.mode {
match self {
Self::Cancel => {
view.mode = None;
},
Self::Prev => {
focus.prev()
},
Self::Next => {
focus.next()
},
Self::Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Self::Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Self::Set(length) => {
let mut phrase = view.phrases[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
view.mode = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
}
Ok(None)
} else if self == Begin {
view.mode = Some(PhrasesMode::Length(
view.phrase,
view.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
));
Ok(None)
} else {
unreachable!()
}
}
}
impl InputToCommand<Tui, PhrasesTui> for PhraseRenameCommand {
fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option<Self> {
if let Some(PhrasesMode::Rename(_, ref old_name)) = view.mode {
Some(match from.event() {
key!(KeyCode::Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key!(KeyCode::Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key!(KeyCode::Enter) => Self::Confirm,
key!(KeyCode::Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl Command<PhrasesTui> for PhraseRenameCommand {
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
use PhraseRenameCommand::*;
if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = view.mode {
match self {
Set(s) => {
view.phrases[phrase].write().unwrap().name = s.into();
return Ok(Some(Self::Set(old_name.clone())))
},
Confirm => {
let old_name = old_name.clone();
view.mode = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
let mut phrase = view.phrases[phrase].write().unwrap();
phrase.name = old_name.clone();
},
_ => unreachable!()
};
Ok(None)
} else if self == Begin {
view.mode = Some(PhrasesMode::Rename(
view.phrase,
view.phrases[view.phrase].read().unwrap().name.clone()
));
Ok(None)
} else {
unreachable!()
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(usize),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(usize),
TimeScrollSet(usize),
TimeZoomSet(usize),
}
impl InputToCommand<Tui, PhraseTui> for PhraseCommand {
fn input_to_command (state: &PhraseTui, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
Some(match from.event() {
key!(KeyCode::Char('`')) => ToggleDirection,
key!(KeyCode::Enter) => EnterEditMode,
key!(KeyCode::Esc) => ExitEditMode,
key!(KeyCode::Char('[')) => NoteLengthSet(0),
key!(KeyCode::Char(']')) => NoteLengthSet(0),
key!(KeyCode::Char('a')) => NoteAppend,
key!(KeyCode::Char('s')) => NoteSet,
key!(KeyCode::Char('-')) => TimeZoomSet(0),
key!(KeyCode::Char('_')) => TimeZoomSet(0),
key!(KeyCode::Char('=')) => TimeZoomSet(0),
key!(KeyCode::Char('+')) => TimeZoomSet(0),
key!(KeyCode::PageUp) => NoteScrollSet(0),
key!(KeyCode::PageDown) => NoteScrollSet(0),
key!(KeyCode::Up) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Down) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Left) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
key!(KeyCode::Right) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
_ => return None
})
}
}
impl Command<PhraseTui> for PhraseCommand {
//fn translate (self, state: &PhraseTui<E>) -> Self {
//use PhraseCommand::*;
//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 PhraseTui) -> Perhaps<Self> {
use PhraseCommand::*;
match self.translate(state) {
ToggleDirection => {
state.mode = !state.mode;
},
EnterEditMode => {
state.entered = true;
},
ExitEditMode => {
state.entered = false;
},
TimeZoomOut => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = next_note_length(scale)
},
TimeZoomIn => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = prev_note_length(scale)
},
TimeCursorDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_dec(scale);
},
TimeCursorInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_inc(scale);
},
TimeScrollDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_dec(scale);
},
TimeScrollInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_inc(scale);
},
NoteCursorDec => {
let mut axis = state.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
},
NoteCursorInc => {
let mut axis = state.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
},
NoteScrollDec => {
state.note_axis.write().unwrap().start_inc(1);
},
NoteScrollInc => {
state.note_axis.write().unwrap().start_dec(1);
},
NoteLengthDec => {
state.note_len = prev_note_length(state.note_len)
},
NoteLengthInc => {
state.note_len = next_note_length(state.note_len)
},
NotePageUp => {
let mut axis = state.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
},
NotePageDown => {
let mut axis = state.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
},
NoteAppend => {
if state.entered {
state.put();
state.time_cursor_advance();
}
},
NoteSet => {
if state.entered { state.put(); }
},
_ => unreachable!()
}
Ok(None)
}
}

View file

@ -213,3 +213,31 @@ impl FocusGrid for ArrangerTui {
}
}
}
/// 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,
}
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

@ -98,3 +98,100 @@ pub struct ArrangerTui {
pub status_bar: Option<ArrangerStatus>,
pub history: Vec<ArrangerCommand>,
}
#[derive(Default, Debug, Clone)]
pub struct ArrangerScene {
/// 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(Debug)]
pub struct ArrangerTrack {
/// Name of track
name: Arc<RwLock<String>>,
/// Preferred width of track column
width: usize,
/// Identifying color of track
color: ItemColor,
/// Start time and phrase being played
play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
monitoring: bool,
/// Write input to sequence.
recording: bool,
/// Overdub input to sequence.
overdub: bool,
/// Send all notes off
reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
midi_ins: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
midi_outs: Vec<Port<MidiOut>>,
/// Notes currently held at input
notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
notes_out: Arc<RwLock<[bool; 128]>>,
///// MIDI output buffer
//midi_note: Vec<u8>,
///// MIDI output buffer
//midi_chunk: Vec<Vec<Vec<u8>>>,
}
pub struct PhrasesTui {
/// Collection of phrases
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub phrase: usize,
/// Scroll offset
pub scroll: usize,
/// Mode switch
pub mode: Option<PhrasesMode>,
/// Whether this widget is focused
pub focused: bool,
/// Whether this widget is entered
pub entered: bool,
}
/// Modes for phrase pool
pub enum PhrasesMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
}
/// Contains state for viewing and editing a phrase
pub struct PhraseTui {
/// 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 and height of notes area at last render
pub size: Measure<Tui>
}

View file

@ -1,45 +1,5 @@
use crate::*;
/// Contains state for viewing and editing a phrase
pub struct PhraseTui {
/// 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 and height of notes area at last render
pub size: Measure<Tui>
}
impl Widget for PhraseTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhraseView(&self, Default::default()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhraseView(&self, Default::default()).render(to)
}
}
impl PhraseTui {
pub fn new () -> Self {
Self {
@ -179,48 +139,3 @@ impl PhraseTui {
}
}
}
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
];
pub(crate) fn keys_vert () -> Buffer {
let area = [0, 0, 5, 64];
let mut buffer = Buffer::empty(Rect {
x: area.x(), y: area.y(), width: area.w(), height: area.h()
});
buffer_update(&mut buffer, area, &|cell, x, y| {
let y = 63 - y;
match x {
0 => {
cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_char('▀');
cell.set_fg(Color::White);
cell.set_bg(Color::White);
},
2 => if y % 6 == 0 {
cell.set_char('C');
},
3 => if y % 6 == 0 {
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
},
_ => {}
}
});
buffer
}
const NTH_OCTAVE: [&'static str; 11] = [
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
];

View file

@ -1,149 +0,0 @@
use crate::*;
#[derive(Clone, PartialEq, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(usize),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(usize),
TimeScrollSet(usize),
TimeZoomSet(usize),
}
impl InputToCommand<Tui, PhraseTui> for PhraseCommand {
fn input_to_command (state: &PhraseTui, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
Some(match from.event() {
key!(KeyCode::Char('`')) => ToggleDirection,
key!(KeyCode::Enter) => EnterEditMode,
key!(KeyCode::Esc) => ExitEditMode,
key!(KeyCode::Char('[')) => NoteLengthSet(0),
key!(KeyCode::Char(']')) => NoteLengthSet(0),
key!(KeyCode::Char('a')) => NoteAppend,
key!(KeyCode::Char('s')) => NoteSet,
key!(KeyCode::Char('-')) => TimeZoomSet(0),
key!(KeyCode::Char('_')) => TimeZoomSet(0),
key!(KeyCode::Char('=')) => TimeZoomSet(0),
key!(KeyCode::Char('+')) => TimeZoomSet(0),
key!(KeyCode::PageUp) => NoteScrollSet(0),
key!(KeyCode::PageDown) => NoteScrollSet(0),
key!(KeyCode::Up) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Down) => match state.entered {
true => NoteCursorSet(0),
false => NoteScrollSet(0),
},
key!(KeyCode::Left) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
key!(KeyCode::Right) => match state.entered {
true => TimeCursorSet(0),
false => TimeScrollSet(0),
},
_ => return None
})
}
}
impl Command<PhraseTui> for PhraseCommand {
//fn translate (self, state: &PhraseTui<E>) -> Self {
//use PhraseCommand::*;
//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 PhraseTui) -> Perhaps<Self> {
use PhraseCommand::*;
match self.translate(state) {
ToggleDirection => {
state.mode = !state.mode;
},
EnterEditMode => {
state.entered = true;
},
ExitEditMode => {
state.entered = false;
},
TimeZoomOut => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = next_note_length(scale)
},
TimeZoomIn => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().scale = prev_note_length(scale)
},
TimeCursorDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_dec(scale);
},
TimeCursorInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().point_inc(scale);
},
TimeScrollDec => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_dec(scale);
},
TimeScrollInc => {
let scale = state.time_axis.read().unwrap().scale;
state.time_axis.write().unwrap().start_inc(scale);
},
NoteCursorDec => {
let mut axis = state.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
},
NoteCursorInc => {
let mut axis = state.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
},
NoteScrollDec => {
state.note_axis.write().unwrap().start_inc(1);
},
NoteScrollInc => {
state.note_axis.write().unwrap().start_dec(1);
},
NoteLengthDec => {
state.note_len = prev_note_length(state.note_len)
},
NoteLengthInc => {
state.note_len = next_note_length(state.note_len)
},
NotePageUp => {
let mut axis = state.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
},
NotePageDown => {
let mut axis = state.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
},
NoteAppend => {
if state.entered {
state.put();
state.time_cursor_advance();
}
},
NoteSet => {
if state.entered { state.put(); }
},
_ => unreachable!()
}
Ok(None)
}
}

View file

@ -1,183 +0,0 @@
use crate::*;
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
pub trait PhraseViewState: Send + Sync {
fn focused (&self) -> bool;
fn entered (&self) -> bool;
fn keys (&self) -> &Buffer;
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn buffer (&self) -> &BigBuffer;
fn note_len (&self) -> usize;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn size (&self) -> &Measure<Tui>;
fn now (&self) -> &Arc<Pulse>;
}
impl PhraseViewState for PhraseTui {
fn focused (&self) -> bool {
&self.focused
}
fn entered (&self) -> bool {
&self.entered
}
fn keys (&self) -> &Buffer {
&self.keys
}
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
&self.phrase
}
fn buffer (&self) -> &BigBuffer {
&self.buffer
}
fn note_len (&self) -> usize {
&self.note_len
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self.note_axis
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self.time_axis
}
fn size (&self) -> &Measure<Tui> {
&self.size
}
fn now (&self) -> &Arc<Pulse> {
&self.now
}
}
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let phrase = self.0.phrase();
let size = self.0.size();
let focused = self.0.focused();
let entered = self.0.entered();
let keys = self.0.keys();
let buffer = self.0.buffer();
let note_len = self.0.note_len();
let note_axis = self.0.note_axis();
let time_axis = self.0.time_axis();
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
= *note_axis.read().unwrap();
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
= *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;
size.set_wh(area.w(), h);
let mut axis = 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 + (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.0.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!("{}", size.format());
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}",
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(),
)
}
}

View file

@ -1,190 +0,0 @@
use crate::*;
pub struct PhrasesTui {
/// Collection of phrases
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub phrase: usize,
/// Scroll offset
pub scroll: usize,
/// Mode switch
pub mode: Option<PhrasesMode>,
/// Whether this widget is focused
pub focused: bool,
/// Whether this widget is entered
pub entered: bool,
}
/// Modes for phrase pool
pub enum PhrasesMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
}
impl PhrasesTui {
pub fn new (phrases: Vec<Arc<RwLock<Phrase>>>) -> Self {
Self {
scroll: 0,
phrase: 0,
mode: None,
focused: false,
entered: false,
phrases,
}
}
pub fn len (&self) -> usize {
self.phrases.len()
}
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> {
&self.phrases[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 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;
}
}
}
/// Displays and edits phrase length.
pub struct PhraseLength {
/// 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>,
}
impl PhraseLength {
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 Content for PhraseLength {
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(),
"]"
)),
}
})
}
}
/// 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,
}
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

@ -1,221 +0,0 @@
use crate::*;
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
Select(usize),
Edit(PhrasePoolCommand),
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Set(String),
Confirm,
Cancel,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Next,
Prev,
Inc,
Dec,
Set(usize),
Cancel,
}
impl InputToCommand<Tui, PhrasesTui> for PhrasesCommand {
fn input_to_command (state: &PhrasesTui, input: &TuiInput) -> Option<Self> {
use PhrasesCommand as Cmd;
use PhrasePoolCommand as Edit;
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match input.event() {
key!(KeyCode::Up) => Some(Cmd::Select(0)),
key!(KeyCode::Down) => Some(Cmd::Select(0)),
key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))),
key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))),
key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))),
key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))),
key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))),
key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))),
key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)),
key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)),
_ => match state.mode {
Some(PhrasesMode::Rename(..)) => {
Rename::input_to_command(state, input).map(Cmd::Rename)
},
Some(PhrasesMode::Length(..)) => {
Length::input_to_command(state, input).map(Cmd::Length)
},
_ => None
}
}
}
}
impl Command<PhrasesTui> for PhrasesCommand {
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
use PhraseRenameCommand as Rename;
use PhraseLengthCommand as Length;
match self {
Self::Select(phrase) => {
view.phrase = phrase
},
Self::Edit(command) => {
return Ok(command.execute(&mut view)?.map(Self::Edit))
}
Self::Rename(command) => match command {
Rename::Begin => {
view.mode = Some(PhrasesMode::Rename(
view.phrase,
view.phrases[view.phrase].read().unwrap().name.clone()
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Rename))
}
},
Self::Length(command) => match command {
Length::Begin => {
view.mode = Some(PhrasesMode::Length(
view.phrase,
view.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
))
},
_ => {
return Ok(command.execute(view)?.map(Self::Length))
}
},
}
Ok(None)
}
}
impl InputToCommand<Tui, PhrasesTui> for PhraseLengthCommand {
fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option<Self> {
if let Some(PhrasesMode::Length(_, length, _)) = view.mode {
Some(match from.event() {
key!(KeyCode::Up) => Self::Inc,
key!(KeyCode::Down) => Self::Dec,
key!(KeyCode::Right) => Self::Next,
key!(KeyCode::Left) => Self::Prev,
key!(KeyCode::Enter) => Self::Set(length),
key!(KeyCode::Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl Command<PhrasesTui> for PhraseLengthCommand {
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = view.mode {
match self {
Self::Cancel => {
view.mode = None;
},
Self::Prev => {
focus.prev()
},
Self::Next => {
focus.next()
},
Self::Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Self::Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Self::Set(length) => {
let mut phrase = view.phrases[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
view.mode = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
}
Ok(None)
} else if self == Begin {
view.mode = Some(PhrasesMode::Length(
view.phrase,
view.phrases[view.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
));
Ok(None)
} else {
unreachable!()
}
}
}
impl InputToCommand<Tui, PhrasesTui> for PhraseRenameCommand {
fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option<Self> {
if let Some(PhrasesMode::Rename(_, ref old_name)) = view.mode {
Some(match from.event() {
key!(KeyCode::Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key!(KeyCode::Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key!(KeyCode::Enter) => Self::Confirm,
key!(KeyCode::Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}
impl Command<PhrasesTui> for PhraseRenameCommand {
fn execute (self, view: &mut PhrasesTui) -> Perhaps<Self> {
use PhraseRenameCommand::*;
if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = view.mode {
match self {
Set(s) => {
view.phrases[phrase].write().unwrap().name = s.into();
return Ok(Some(Self::Set(old_name.clone())))
},
Confirm => {
let old_name = old_name.clone();
view.mode = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
let mut phrase = view.phrases[phrase].write().unwrap();
phrase.name = old_name.clone();
},
_ => unreachable!()
};
Ok(None)
} else if self == Begin {
view.mode = Some(PhrasesMode::Rename(
view.phrase,
view.phrases[view.phrase].read().unwrap().name.clone()
));
Ok(None)
} else {
unreachable!()
}
}
}

View file

@ -1,88 +0,0 @@
use crate::*;
impl Widget for PhrasesTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhrasesView(&self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhrasesView(&self).render(to)
}
}
pub trait PhrasesViewState {
fn focused (&self) -> bool;
fn entered (&self) -> bool;
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>>;
fn phrase (&self) -> usize;
fn mode (&self) -> Option<&PhrasesMode>;
}
impl PhrasesViewState for PhrasesTui {
fn focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn mode (&self) -> Option<&PhrasesMode> {
&self.mode
}
}
pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T);
// TODO: Display phrases always in order of appearance
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.0.focused();
let entered = self.0.entered();
let phrases = self.0.phrases();
let selected_phrase = self.0.phrase();
let mode = self.0.mode();
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(PhrasesMode::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(PhrasesMode::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))?;
if focused && i == selected_phrase {
add(&CORNERS)?;
}
Ok(())
})
);
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 entered {""} else {" "});
let upper_right = format!("({})", self.0.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(),
)
}
}

View file

@ -1,31 +1 @@
use crate::*;
impl HasPhrases for SequencerTui {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases
}
}
impl HasPhrase for SequencerTui {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn phrase_mut (&self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase (&self) -> &Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option<Arc<RwLock<Phrase>>>)> {
todo!()
}
}

View file

@ -1,64 +0,0 @@
use crate::*;
#[derive(Clone, Debug, PartialEq)]
pub enum SequencerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Clock(ClockCommand),
Playhead(PlayheadCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
use FocusCommand::*;
use SequencerCommand::*;
match input.event() {
key!(KeyCode::Tab) => Some(Self::Focus(Next)),
key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)),
key!(KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)),
key!(KeyCode::Up) => Some(Self::Focus(Up)),
key!(KeyCode::Down) => Some(Self::Focus(Down)),
key!(KeyCode::Left) => Some(Self::Focus(Left)),
key!(KeyCode::Right) => Some(Self::Focus(Right)),
_ => Some(Self::App(match state.focused() {
SequencerFocus::Transport =>
TransportCommand::input_to_command(&state, input).map(Transport),
SequencerFocus::Phrases =>
PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases),
SequencerFocus::PhraseEditor =>
PhraseCommand::input_to_command(&state.editor, input).map(Editor),
_ => return None,
}))
}
}
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
use SequencerCommand::*;
match self {
Focus(cmd) => delegate(cmd, Focus, state),
Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases),
Editor(cmd) => delegate(cmd, Editor, &mut state.editor),
Clock(cmd) => delegate(cmd, Clock, &mut state.transport),
Playhead(cmd) => delegate(cmd, Playhead, &mut state.transport)
}
}
}
impl TransportControl for SequencerTui {
fn bpm (&self) -> &BeatsPerMinute {
self.app.bpm()
}
fn quant (&self) -> &Quantize {
self.app.quant()
}
fn sync (&self) -> &LaunchSync {
self.app.sync()
}
}

View file

@ -1,89 +0,0 @@
use crate::*;
impl Content for SequencerTui {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
col!(
widget(&TransportView(self)),
Split::right(20,
widget(&PhrasesView(self)),
widget(&PhraseView(self)),
).min_y(20)
)
}
}
impl TransportViewState for SequencerTui {
fn focus (&self) -> TransportFocus {
self.focus
}
fn is_focused (&self) -> bool {
self.focused
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
fn bpm_value (&self) -> f64 {
self.bpm().get()
}
fn sync_value (&self) -> f64 {
self.sync().get()
}
fn format_beat (&self) -> String {
self.current().format_beat()
}
fn format_msu (&self) -> String {
self.current().usec.format_msu()
}
}
impl PhrasesViewState for SequencerTui {
fn focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn mode (&self) -> Option<&PhrasesMode> {
&self.mode
}
}
impl PhraseViewState for SequencerTui {
fn focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn keys (&self) -> &Buffer {
todo!()
}
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
todo!()
}
fn buffer (&self) -> &BigBuffer {
todo!()
}
fn note_len (&self) -> usize {
todo!()
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
todo!()
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
todo!()
}
fn size (&self) -> &Measure<Tui> {
todo!()
}
fn now (&self) -> &Arc<Pulse> {
todo!()
}
}

View file

@ -1 +0,0 @@
use crate::*;

View file

@ -1,101 +0,0 @@
use crate::*;
#[derive(Clone, Debug, PartialEq)]
pub enum TransportCommand {
Focus(FocusCommand),
Clock(ClockCommand),
Playhead(PlayheadCommand),
}
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
use KeyCode::Char;
use ClockCommand::{SetBpm, SetQuant, SetSync};
use TransportFocus as Focused;
use TransportCommand::{Focus, Clock, Playhead};
let focused = state.focused();
Some(match input.event() {
key!(Left) => Focus(FocusCommand::Prev),
key!(Right) => Focus(FocusCommand::Next),
key!(Char('.')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() + 1.0)),
Focused::Quant => Clock(SetQuant(state.next_quant())),
Focused::Sync => Clock(SetSync(state.next_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
key!(KeyCode::Char(',')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() - 1.0)),
Focused::Quant => Clock(SetQuant(state.prev_quant())),
Focused::Sync => Clock(SetSync(state.prev_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
key!(KeyCode::Char('>')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() + 0.001)),
Focused::Quant => Clock(SetQuant(state.next_quant())),
Focused::Sync => Clock(SetSync(state.next_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
key!(KeyCode::Char('<')) => match focused {
Focused::Bpm => Clock(SetBpm(state.bpm().get() - 0.001)),
Focused::Quant => Clock(SetQuant(state.prev_quant())),
Focused::Sync => Clock(SetSync(state.prev_sync())),
Focused::PlayPause => Playhead(todo!()),
Focused::Clock => Playhead(todo!()),
_ => {todo!()}
},
_ => return None
})
}
}
pub trait TransportControl {
fn quant (&self) -> &Quantize;
fn bpm (&self) -> &BeatsPerMinute;
fn next_quant (&self) -> f64 {
next_note_length(self.quant().get() as usize) as f64
}
fn prev_quant (&self) -> f64 {
prev_note_length(self.quant().get() as usize) as f64
}
fn sync (&self) -> &LaunchSync;
fn next_sync (&self) -> f64 {
next_note_length(self.sync().get() as usize) as f64
}
fn prev_sync (&self) -> f64 {
prev_note_length(self.sync().get() as usize) as f64
}
}
impl TransportControl for TransportTui {
fn bpm (&self) -> &BeatsPerMinute {
self.bpm()
}
fn quant (&self) -> &Quantize {
self.quant()
}
fn sync (&self) -> &LaunchSync {
self.sync()
}
}
impl<T: TransportControl> Command<T> for TransportCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use TransportCommand::{Focus, Clock, Playhead};
use ClockCommand::{SetBpm, SetQuant, SetSync};
Ok(Some(match self {
Focus(Next) => { todo!() }
Focus(Prev) => { todo!() },
Focus(_) => { unimplemented!() },
Clock(SetBpm(bpm)) => Clock(SetBpm(state.bpm().set(bpm))),
Clock(SetQuant(quant)) => Clock(SetQuant(state.quant().set(quant))),
Clock(SetSync(sync)) => Clock(SetSync(state.sync().set(sync))),
_ => return Ok(None)
}))
}
}

View file

@ -1,85 +0,0 @@
use crate::*;
impl Widget for TransportTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
TransportView(&self, Default::default()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
TransportView(&self, Default::default()).render(to)
}
}
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
pub trait TransportViewState: Send + Sync {
fn focus (&self) -> TransportFocus;
fn is_focused (&self) -> bool;
fn transport_state (&self) -> Option<TransportState>;
fn bpm_value (&self) -> f64;
fn sync_value (&self) -> f64;
fn format_beat (&self) -> String;
fn format_msu (&self) -> String;
}
impl TransportViewState for TransportTui {
fn focus (&self) -> TransportFocus {
self.focus
}
fn is_focused (&self) -> bool {
self.focused
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
fn bpm_value (&self) -> f64 {
self.bpm().get()
}
fn sync_value (&self) -> f64 {
self.sync().get()
}
fn format_beat (&self) -> String {
self.current().format_beat()
}
fn format_msu (&self) -> String {
self.current().usec.format_msu()
}
}
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let state = self.0;
lay!(
state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled(
None,
match state.transport_state() {
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!(
state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, {
let bpm = state.bpm_value();
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
})),
//let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! {
//"QUANT ", ppq_to_name(state.0.quant as usize)
//})),
state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! {
"SYNC ", pulses_to_name(state.sync_value() as usize)
}))
).align_w().fill_x(),
state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{
let time1 = state.format_beat();
let time2 = state.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))
}
}

View file

@ -1,5 +1,177 @@
use crate::*;
impl Widget for TransportTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
TransportView(&self, Default::default()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
TransportView(&self, Default::default()).render(to)
}
}
pub struct TransportView<'a, T: TransportViewState>(pub &'a T);
pub trait TransportViewState: Send + Sync {
fn focus (&self) -> TransportFocus;
fn is_focused (&self) -> bool;
fn transport_state (&self) -> Option<TransportState>;
fn bpm_value (&self) -> f64;
fn sync_value (&self) -> f64;
fn format_beat (&self) -> String;
fn format_msu (&self) -> String;
}
impl TransportViewState for TransportTui {
fn focus (&self) -> TransportFocus {
self.focus
}
fn is_focused (&self) -> bool {
self.focused
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
fn bpm_value (&self) -> f64 {
self.bpm().get()
}
fn sync_value (&self) -> f64 {
self.sync().get()
}
fn format_beat (&self) -> String {
self.current().format_beat()
}
fn format_msu (&self) -> String {
self.current().usec.format_msu()
}
}
impl<'a, T: TransportViewState> Content for TransportView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let state = self.0;
lay!(
state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled(
None,
match state.transport_state() {
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!(
state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, {
let bpm = state.bpm_value();
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
})),
//let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! {
//"QUANT ", ppq_to_name(state.0.quant as usize)
//})),
state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! {
"SYNC ", pulses_to_name(state.sync_value() as usize)
}))
).align_w().fill_x(),
state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{
let time1 = state.format_beat();
let time2 = state.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 Content for SequencerTui {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
col!(
widget(&TransportView(self)),
Split::right(20,
widget(&PhrasesView(self)),
widget(&PhraseView(self)),
).min_y(20)
)
}
}
impl TransportViewState for SequencerTui {
fn focus (&self) -> TransportFocus {
self.focus
}
fn is_focused (&self) -> bool {
self.focused
}
fn transport_state (&self) -> Option<TransportState> {
*self.playing().read().unwrap()
}
fn bpm_value (&self) -> f64 {
self.bpm().get()
}
fn sync_value (&self) -> f64 {
self.sync().get()
}
fn format_beat (&self) -> String {
self.current().format_beat()
}
fn format_msu (&self) -> String {
self.current().usec.format_msu()
}
}
impl PhrasesViewState for SequencerTui {
fn focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn mode (&self) -> Option<&PhrasesMode> {
&self.mode
}
}
impl PhraseViewState for SequencerTui {
fn focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn keys (&self) -> &Buffer {
todo!()
}
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
todo!()
}
fn buffer (&self) -> &BigBuffer {
todo!()
}
fn note_len (&self) -> usize {
todo!()
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
todo!()
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
todo!()
}
fn size (&self) -> &Measure<Tui> {
todo!()
}
fn now (&self) -> &Arc<Pulse> {
todo!()
}
}
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
@ -531,3 +703,397 @@ impl PhraseViewState for ArrangerTui {
todo!()
}
}
impl Widget for PhrasesTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhrasesView(&self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhrasesView(&self).render(to)
}
}
pub trait PhrasesViewState {
fn focused (&self) -> bool;
fn entered (&self) -> bool;
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>>;
fn phrase (&self) -> usize;
fn mode (&self) -> Option<&PhrasesMode>;
}
impl PhrasesViewState for PhrasesTui {
fn focused (&self) -> bool {
todo!()
}
fn entered (&self) -> bool {
todo!()
}
fn phrases (&self) -> Vec<Arc<RwLock<Phrase>>> {
todo!()
}
fn phrase (&self) -> usize {
todo!()
}
fn mode (&self) -> Option<&PhrasesMode> {
&self.mode
}
}
pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T);
// TODO: Display phrases always in order of appearance
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.0.focused();
let entered = self.0.entered();
let phrases = self.0.phrases();
let selected_phrase = self.0.phrase();
let mode = self.0.mode();
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(PhrasesMode::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(PhrasesMode::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))?;
if focused && i == selected_phrase {
add(&CORNERS)?;
}
Ok(())
})
);
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 entered {""} else {" "});
let upper_right = format!("({})", self.0.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(),
)
}
}
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
pub trait PhraseViewState: Send + Sync {
fn focused (&self) -> bool;
fn entered (&self) -> bool;
fn keys (&self) -> &Buffer;
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn buffer (&self) -> &BigBuffer;
fn note_len (&self) -> usize;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn size (&self) -> &Measure<Tui>;
fn now (&self) -> &Arc<Pulse>;
}
impl PhraseViewState for PhraseTui {
fn focused (&self) -> bool {
&self.focused
}
fn entered (&self) -> bool {
&self.entered
}
fn keys (&self) -> &Buffer {
&self.keys
}
fn phrase (&self) -> &Option<Arc<RwLock<Phrase>>> {
&self.phrase
}
fn buffer (&self) -> &BigBuffer {
&self.buffer
}
fn note_len (&self) -> usize {
&self.note_len
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self.note_axis
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self.time_axis
}
fn size (&self) -> &Measure<Tui> {
&self.size
}
fn now (&self) -> &Arc<Pulse> {
&self.now
}
}
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let phrase = self.0.phrase();
let size = self.0.size();
let focused = self.0.focused();
let entered = self.0.entered();
let keys = self.0.keys();
let buffer = self.0.buffer();
let note_len = self.0.note_len();
let note_axis = self.0.note_axis();
let time_axis = self.0.time_axis();
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
= *note_axis.read().unwrap();
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
= *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;
size.set_wh(area.w(), h);
let mut axis = 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 + (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.0.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!("{}", size.format());
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}",
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(),
)
}
}
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
];
pub(crate) fn keys_vert () -> Buffer {
let area = [0, 0, 5, 64];
let mut buffer = Buffer::empty(Rect {
x: area.x(), y: area.y(), width: area.w(), height: area.h()
});
buffer_update(&mut buffer, area, &|cell, x, y| {
let y = 63 - y;
match x {
0 => {
cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_char('▀');
cell.set_fg(Color::White);
cell.set_bg(Color::White);
},
2 => if y % 6 == 0 {
cell.set_char('C');
},
3 => if y % 6 == 0 {
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
},
_ => {}
}
});
buffer
}
const NTH_OCTAVE: [&'static str; 11] = [
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
];
impl Content for PhraseLength {
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(),
"]"
)),
}
})
}
}
/// Displays and edits phrase length.
pub struct PhraseLength {
/// 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>,
}
impl PhraseLength {
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 Widget for PhraseTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhraseView(&self, Default::default()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhraseView(&self, Default::default()).render(to)
}
}