compaaaaact

This commit is contained in:
🪞👃🪞 2024-12-09 23:57:49 +01:00
parent 4ce4742959
commit 271f431a6a
25 changed files with 1367 additions and 1361 deletions

View file

@ -1,4 +1,12 @@
use crate::*;
use crate::{
*,
api::{
ArrangerTrackCommand,
ArrangerSceneCommand,
ArrangerClipCommand
}
};
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
type Error = Box<dyn std::error::Error>;
@ -853,3 +861,451 @@ pub fn arranger_content_horizontal (
//)
//)
//}
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])
}
fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
}
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
}
}
#[derive(Default, Debug, Clone)]
pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<RwLock<String>>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
/// Identifying color of scene
pub(crate) 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
}
}
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()),
player: PhrasePlayerModel::from(&self.clock),
};
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
pub(crate) name: Arc<RwLock<String>>,
/// Preferred width of track column
pub(crate) width: usize,
/// Identifying color of track
pub(crate) color: ItemColor,
/// MIDI player state
pub(crate) player: PhrasePlayerModel,
}
impl HasPlayer for ArrangerTrack {
fn player (&self) -> &impl MidiPlayerApi {
&self.player
}
fn player_mut (&mut self) -> &mut impl MidiPlayerApi {
&mut self.player
}
}
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
}
}
#[derive(PartialEq, Clone, Copy, Debug)]
/// Represents the current user selection in the arranger
pub enum ArrangerSelection {
/// The whole mix is selected
Mix,
/// A track is selected.
Track(usize),
/// A scene is selected.
Scene(usize),
/// A clip (track × scene) is selected.
Clip(usize, usize),
}
/// Focus identification methods
impl ArrangerSelection {
pub fn description <E: Engine> (
&self,
tracks: &Vec<ArrangerTrack>,
scenes: &Vec<ArrangerScene>,
) -> String {
format!("Selected: {}", match self {
Self::Mix => format!("Everything"),
Self::Track(t) => match tracks.get(*t) {
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
None => format!("T??"),
},
Self::Scene(s) => match scenes.get(*s) {
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
None => format!("S??"),
},
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
(Some(_), Some(scene)) => match scene.clip(*t) {
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
None => format!("T{t} S{s}: Empty")
},
_ => format!("T{t} S{s}: Empty"),
}
})
}
pub fn is_mix (&self) -> bool {
match self { Self::Mix => true, _ => false }
}
pub fn is_track (&self) -> bool {
match self { Self::Track(_) => true, _ => false }
}
pub fn is_scene (&self) -> bool {
match self { Self::Scene(_) => true, _ => false }
}
pub fn is_clip (&self) -> bool {
match self { Self::Clip(_, _) => true, _ => false }
}
pub fn track (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(t, _) => Some(*t),
Track(t) => Some(*t),
_ => None
}
}
pub fn scene (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(_, s) => Some(*s),
Scene(s) => Some(*s),
_ => None
}
}
}
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Color(ItemColor),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<ArrangerTui> for ArrangerCommand {
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
use ArrangerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Scene(cmd) => cmd.execute(state)?.map(Scene),
Track(cmd) => cmd.execute(state)?.map(Track),
Clip(cmd) => cmd.execute(state)?.map(Clip),
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(_) => { todo!(); },
Select(selected) => {
*state.selected_mut() = selected;
None
},
_ => { todo!() }
})
}
}
impl Command<ArrangerTui> for ArrangerSceneCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerTrackCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerClipCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
pub trait ArrangerControl: TransportControl {
fn selected (&self) -> ArrangerSelection;
fn selected_mut (&mut self) -> &mut ArrangerSelection;
fn activate (&mut self) -> Usually<()>;
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
fn toggle_loop (&mut self);
fn randomize_color (&mut self);
}
impl ArrangerControl for ArrangerTui {
fn selected (&self) -> ArrangerSelection {
self.selected
}
fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
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 InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
to_arranger_command(state, input)
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
}
}
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
))),
_ => match state.focused() {
ArrangerFocus::Transport(_) => {
match TransportCommand::input_to_command(state, input)? {
TransportCommand::Clock(command) => Cmd::Clock(command),
_ => return None,
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
},
ArrangerFocus::Arranger => {
use ArrangerSelection::*;
match input.event() {
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
key!(Char('+')) => Cmd::Zoom(0), // TODO
key!(Char('=')) => Cmd::Zoom(0), // TODO
key!(Char('_')) => Cmd::Zoom(0), // TODO
key!(Char('-')) => Cmd::Zoom(0), // TODO
key!(Char('`')) => { todo!("toggle state mode") },
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
_ => match state.selected() {
Mix => to_arranger_mix_command(input)?,
Track(t) => to_arranger_track_command(input, t)?,
Scene(s) => to_arranger_scene_command(input, s)?,
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
}
}
}
}
})
}
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
Some(match input.event() {
key!(Down) => Cmd::Select(Select::Scene(0)),
key!(Right) => Cmd::Select(Select::Track(0)),
key!(Char(',')) => Cmd::Zoom(0),
key!(Char('.')) => Cmd::Zoom(0),
key!(Char('<')) => Cmd::Zoom(0),
key!(Char('>')) => Cmd::Zoom(0),
key!(Delete) => Cmd::Clear,
key!(Char('c')) => Cmd::Color(ItemColor::random()),
_ => return None
})
}
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
Some(match input.event() {
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
key!(Right) => Cmd::Select(Select::Track(t + 1)),
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
key!(Delete) => Cmd::Track(Track::Delete(t)),
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
_ => return None
})
}
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerSceneCommand as Scene;
Some(match input.event() {
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
key!(Right) => Cmd::Select(Select::Clip(0, s)),
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key!(Enter) => Cmd::Scene(Scene::Play(s)),
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
_ => return None
})
}
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerClipCommand as Clip;
Some(match input.event() {
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
_ => return None
})
}

View file

@ -1,17 +1,30 @@
use crate::*;
use crate::{*, api::ClockCommand::{Play, Pause}};
use super::phrase_editor::PhraseCommand::Show;
use super::app_transport::TransportCommand;
use KeyCode::{Char, Enter};
use SequencerCommand::*;
use SequencerFocus::*;
use super::app_transport::TransportFocus::*;
/// Create app state from JACK handle.
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
let clock = ClockModel::from(jack);
let mut phrase = Phrase::default();
phrase.name = "New".into();
phrase.color = ItemColor::random().into();
phrase.set_length(384);
let mut phrases = PhraseListModel::default();
let phrase = Arc::new(RwLock::new(phrase));
phrases.phrases.push(phrase.clone());
phrases.phrase.store(1, Ordering::Relaxed);
let mut editor = PhraseEditorModel::default();
editor.show_phrase(Some(phrase));
Ok(Self {
jack: jack.clone(),
phrases: PhraseListModel::default(),
phrases: phrases,
player: PhrasePlayerModel::from(&clock),
editor: PhraseEditorModel::default(),
editor: editor,
size: Measure::new(),
cursor: (0, 0),
entered: false,
@ -20,7 +33,7 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
note_buf: vec![],
clock,
perf: PerfModel::default(),
focus: FocusState::Focused(SequencerFocus::Transport(TransportFocus::PlayPause))
focus: FocusState::Focused(SequencerFocus::PhraseEditor)
})
}
}
@ -216,6 +229,7 @@ impl StatusBar for SequencerStatusBar {
impl From<&SequencerTui> for SequencerStatusBar {
fn from (state: &SequencerTui) -> Self {
use super::app_transport::TransportFocus::*;
let samples = state.clock.chunk.load(Ordering::Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
let buffer = samples as f64 / rate;
@ -358,3 +372,95 @@ render!(|self:SequencerStats<'a>|{
Tui::fg(orange, size),
]))
});
impl Handle<Tui> for SequencerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum SequencerCommand {
Focus(FocusCommand),
Clock(ClockCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
Enqueue(Option<Arc<RwLock<Phrase>>>),
Clear,
Undo,
Redo,
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
Ok(match self {
Self::Focus(cmd) => cmd.execute(state)?.map(Focus),
Self::Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
Self::Enqueue(phrase) => {
state.player.enqueue_next(phrase.as_ref());
None
},
Self::Undo => { todo!() },
Self::Redo => { todo!() },
Self::Clear => { todo!() },
})
}
}
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
if state.entered() {
to_sequencer_command(state, input)
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
} else {
to_focus_command(input).map(SequencerCommand::Focus)
.or_else(||to_sequencer_command(state, input))
}
}
}
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
Some(match input.event() {
// Play/pause
key!(Char(' ')) => Clock(
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
),
// Play from start/rewind to start
key!(Shift-Char(' ')) => Clock(
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
),
// Edit phrase
key!(Char('e')) => match state.focused() {
SequencerFocus::PhrasePlay => Editor(Show(
state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
)),
SequencerFocus::PhraseNext => Editor(Show(
state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
)),
SequencerFocus::PhraseList => Editor(Show(
Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())
)),
_ => return None,
},
_ => match state.focused() {
SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? {
TransportCommand::Clock(command) => Clock(command),
_ => return None,
},
SequencerFocus::PhraseEditor => Editor(
PhraseCommand::input_to_command(&state.editor, input)?
),
SequencerFocus::PhraseList => match input.event() {
key!(Enter) => Enqueue(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
)),
_ => Phrases(
PhrasesCommand::input_to_command(&state.phrases, input)?
),
}
_ => return None
}
})
}

View file

@ -82,10 +82,10 @@ impl<T: HasClock> From<&T> for TransportView {
bpm,
ppq,
started: true,
global_sample: format!("{:.0}", started.sample.get()),
global_second: format!("{:.0}", started.usec.get()),
current_sample: format!("{:.0}", clock.global.sample.get() - started.sample.get()),
current_second: format!("{:.0}", clock.global.usec.get() - started.usec.get()),
global_sample: format!("{:.0}k", started.sample.get()/1000.),
global_second: format!("{:.1}s", started.usec.get()/1000.),
current_sample: format!("{:.0}k", (clock.global.sample.get() - started.sample.get())/1000.),
current_second: format!("{:.1}s", (clock.global.usec.get() - started.usec.get())/1000000.),
}
} else {
Self {
@ -93,8 +93,8 @@ impl<T: HasClock> From<&T> for TransportView {
bpm,
ppq,
started: false,
global_sample: format!("{:.0}", clock.global.sample.get()),
global_second: format!("{:.0}", clock.global.usec.get()),
global_sample: format!("{:.0}k", clock.global.sample.get()/1000.),
global_second: format!("{:.1}s", clock.global.usec.get()/1000000.),
current_sample: "".to_string(),
current_second: "".to_string(),
}
@ -102,37 +102,43 @@ impl<T: HasClock> From<&T> for TransportView {
}
}
struct TransportField<'a>(&'a str, &'a str);
render!(|self: TransportField<'a>|{
col!([
Tui::fg(Color::Rgb(150, 150, 150), self.0),
Tui::bold(true, Tui::fg(Color::Rgb(200, 200, 200), self.1)),
])
});
render!(|self: TransportView|{
let bg = TuiTheme::border_bg();
let border_style = Style::default().bg(bg).fg(TuiTheme::border_fg(false));
lay!([
Tui::fill_x(Lozenge(border_style)),
Tui::bg(bg, Tui::outset_xy(1, 1, row!([
Tui::outset_x(1, row!([
row!([
col!(["SR", self.sr ]), " ",
col!(["BPM", self.bpm]), " ",
col!(["PPQ", self.ppq]), " ",
]),
row!([
col!(["Sample", self.global_sample]), " ",
col!(["Second", self.global_second]), " ",
TransportField("SR ", self.sr.as_str()),
" ",
TransportField("BPM ", self.bpm.as_str()),
" ",
TransportField("PPQ ", self.ppq.as_str()),
]),
lay!(|add|{
if self.started {
add(&row!([
col!(["", Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING ")]),
" ",
col!(["Sample", self.current_sample]),
TransportField("Beat", "00X+0/0B+00/00P"),
" ",
col!(["Second", self.current_second]),
TransportField("Second", self.current_second.as_str()),
" ",
col!(["Beat", "00B 0b 00/00"]),
TransportField("Sample", self.current_sample.as_str()),
]))
} else {
add(&col!([Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED "), ""]))
}
}),
])))
]))
])
});

View file

@ -1,266 +0,0 @@
use super::model_arranger::ArrangerSelection;
use crate::{
*,
api::{
ArrangerTrackCommand,
ArrangerSceneCommand,
ArrangerClipCommand
}
};
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Undo,
Redo,
Clear,
Color(ItemColor),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<ArrangerTui> for ArrangerCommand {
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
use ArrangerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Scene(cmd) => cmd.execute(state)?.map(Scene),
Track(cmd) => cmd.execute(state)?.map(Track),
Clip(cmd) => cmd.execute(state)?.map(Clip),
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(_) => { todo!(); },
Select(selected) => {
*state.selected_mut() = selected;
None
},
_ => { todo!() }
})
}
}
impl Command<ArrangerTui> for ArrangerSceneCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerTrackCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerClipCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
pub trait ArrangerControl: TransportControl {
fn selected (&self) -> ArrangerSelection;
fn selected_mut (&mut self) -> &mut ArrangerSelection;
fn activate (&mut self) -> Usually<()>;
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
fn toggle_loop (&mut self);
fn randomize_color (&mut self);
}
impl ArrangerControl for ArrangerTui {
fn selected (&self) -> ArrangerSelection {
self.selected
}
fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
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 InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
to_arranger_command(state, input)
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
}
}
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
))),
_ => match state.focused() {
ArrangerFocus::Transport(_) => {
match TransportCommand::input_to_command(state, input)? {
TransportCommand::Clock(command) => Cmd::Clock(command),
_ => return None,
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
},
ArrangerFocus::Arranger => {
use ArrangerSelection::*;
match input.event() {
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
key!(Char('+')) => Cmd::Zoom(0), // TODO
key!(Char('=')) => Cmd::Zoom(0), // TODO
key!(Char('_')) => Cmd::Zoom(0), // TODO
key!(Char('-')) => Cmd::Zoom(0), // TODO
key!(Char('`')) => { todo!("toggle state mode") },
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
_ => match state.selected() {
Mix => to_arranger_mix_command(input)?,
Track(t) => to_arranger_track_command(input, t)?,
Scene(s) => to_arranger_scene_command(input, s)?,
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
}
}
}
}
})
}
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
Some(match input.event() {
key!(Down) => Cmd::Select(Select::Scene(0)),
key!(Right) => Cmd::Select(Select::Track(0)),
key!(Char(',')) => Cmd::Zoom(0),
key!(Char('.')) => Cmd::Zoom(0),
key!(Char('<')) => Cmd::Zoom(0),
key!(Char('>')) => Cmd::Zoom(0),
key!(Delete) => Cmd::Clear,
key!(Char('c')) => Cmd::Color(ItemColor::random()),
_ => return None
})
}
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
Some(match input.event() {
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
key!(Right) => Cmd::Select(Select::Track(t + 1)),
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
key!(Delete) => Cmd::Track(Track::Delete(t)),
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
_ => return None
})
}
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerSceneCommand as Scene;
Some(match input.event() {
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
key!(Right) => Cmd::Select(Select::Clip(0, s)),
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key!(Enter) => Cmd::Scene(Scene::Play(s)),
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
_ => return None
})
}
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerClipCommand as Clip;
Some(match input.event() {
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
_ => return None
})
}

View file

@ -1,129 +0,0 @@
use crate::*;
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
AppendNote,
PutNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
Show(Option<Arc<RwLock<Phrase>>>),
SetEditMode(PhraseEditMode),
ToggleDirection,
}
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
let note_lo = state.note_lo.load(Ordering::Relaxed);
let note_point = state.note_point.load(Ordering::Relaxed);
let time_start = state.time_start.load(Ordering::Relaxed);
let time_point = state.time_point.load(Ordering::Relaxed);
let time_zoom = state.view_mode.time_zoom();
let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('a')) => AppendNote,
key!(Char('s')) => PutNote,
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
key!(Char('n')) => { todo!("toggle keys vs notes") },
_ => match state.edit_mode {
PhraseEditMode::Scroll => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
key!(Up) => SetNoteScroll(note_lo + 1),
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
key!(PageUp) => SetNoteScroll(note_lo + 3),
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
key!(Right) => SetTimeScroll(time_start + 1),
_ => return None
},
PhraseEditMode::Note => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
key!(Up) => SetNoteCursor(note_point + 1),
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key!(PageUp) => SetNoteCursor(note_point + 3),
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
key!(Right) => SetTimeCursor((time_point + time_zoom) % length),
_ => return None
},
}
})
}
}
impl Command<PhraseEditorModel> for PhraseCommand {
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
use PhraseCommand::*;
Ok(match self {
Show(phrase) => {
state.show_phrase(phrase);
None
},
ToggleDirection => {
todo!()
},
SetEditMode(mode) => {
state.edit_mode = mode;
None
}
AppendNote => {
state.put_note();
state.time_cursor_advance();
None
},
PutNote => {
state.put_note();
None
},
SetTimeCursor(time) => {
state.time_point.store(time, Ordering::Relaxed);
None
},
SetTimeScroll(time) => {
state.time_start.store(time, Ordering::Relaxed);
None
},
SetTimeZoom(zoom) => {
state.view_mode.set_time_zoom(zoom);
state.show_phrase(state.phrase.clone());
None
},
SetNoteScroll(note) => {
state.note_lo.store(note, Ordering::Relaxed);
None
},
SetNoteLength(time) => {
state.note_len = time;
None
},
SetNoteCursor(note) => {
let note = 127.min(note);
let start = state.note_lo.load(Ordering::Relaxed);
state.note_point.store(note, Ordering::Relaxed);
if note < start {
state.note_lo.store(note, Ordering::Relaxed);
}
None
},
})
}
}
#[derive(Copy, Clone, Debug)]
pub enum PhraseEditMode {
Note,
Scroll,
}

View file

@ -1,48 +0,0 @@
use crate::*;
use super::model_phrase_list::{PhraseListModel, PhrasesMode};
use super::model_phrase_length::PhraseLengthFocus::*;
use PhraseLengthCommand::*;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
impl Command<PhraseListModel> for PhraseLengthCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
match state.phrases_mode_mut().clone() {
Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self {
Cancel => { *state.phrases_mode_mut() = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let mut phrase = state.phrases()[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
std::mem::drop(phrase);
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
},
_ => unreachable!()
};
Ok(None)
}
}

View file

@ -1,141 +0,0 @@
use crate::{
*,
api::PhrasePoolCommand as Pool,
tui::{
ctrl_phrase_rename::PhraseRenameCommand as Rename,
ctrl_phrase_length::PhraseLengthCommand as Length,
ctrl_file_browser::FileBrowserCommand as Browse,
}
};
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
Select(usize),
Phrase(Pool),
Rename(Rename),
Length(Length),
Import(Browse),
Export(Browse),
}
impl Command<PhraseListModel> for PhrasesCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
use PhrasesCommand::*;
Ok(match self {
Phrase(command) => command.execute(state)?.map(Phrase),
Rename(command) => match command {
PhraseRenameCommand::Begin => {
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
*state.phrases_mode_mut() = Some(
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
);
None
},
_ => command.execute(state)?.map(Rename)
},
Length(command) => match command {
PhraseLengthCommand::Begin => {
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
*state.phrases_mode_mut() = Some(
PhrasesMode::Rename(state.phrase_index(), name)
);
None
},
_ => command.execute(state)?.map(Length)
},
Import(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhrasesMode::Import(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Import)
},
Export(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhrasesMode::Export(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Export)
},
Select(phrase) => {
state.set_phrase_index(phrase);
None
},
})
}
}
impl HasPhrases for PhraseListModel {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases
}
}
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
Some(match state.phrases_mode() {
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
})
}
}
fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PhrasesCommand as Cmd;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key!(Char('n')) => Cmd::Rename(Rename::Begin),
key!(Char('t')) => Cmd::Length(Length::Begin),
key!(Char('m')) => Cmd::Import(Browse::Begin),
key!(Char('x')) => Cmd::Export(Browse::Begin),
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
key!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
key!(Char(',')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index))
} else {
return None
},
key!(Char('.')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(Pool::Swap(index + 1, index))
} else {
return None
},
key!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(Pool::Delete(index))
} else {
return None
},
key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
))),
key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
))),
key!(Char('d')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
Cmd::Phrase(Pool::Add(index + 1, phrase))
},
_ => return None
})
}

View file

@ -1,97 +0,0 @@
use crate::{*, api::ClockCommand::{Play, Pause}};
use super::ctrl_phrase_editor::PhraseCommand::Show;
use KeyCode::{Char, Enter};
use SequencerCommand::*;
use super::app_transport::TransportCommand;
impl Handle<Tui> for SequencerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
SequencerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum SequencerCommand {
Focus(FocusCommand),
Clock(ClockCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
Enqueue(Option<Arc<RwLock<Phrase>>>),
Clear,
Undo,
Redo,
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Enqueue(phrase) => {
state.player.enqueue_next(phrase.as_ref());
None
},
Undo => { todo!() },
Redo => { todo!() },
Clear => { todo!() },
})
}
}
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
if state.entered() {
to_sequencer_command(state, input)
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
} else {
to_focus_command(input).map(SequencerCommand::Focus)
.or_else(||to_sequencer_command(state, input))
}
}
}
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
Some(match input.event() {
// Play/pause
key!(Char(' ')) => Clock(
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
),
// Play from start/rewind to start
key!(Shift-Char(' ')) => Clock(
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
),
// Edit phrase
key!(Char('e')) => match state.focused() {
SequencerFocus::PhrasePlay => Editor(Show(
state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
)),
SequencerFocus::PhraseNext => Editor(Show(
state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
)),
SequencerFocus::PhraseList => Editor(Show(
Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())
)),
_ => return None,
},
_ => match state.focused() {
SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? {
TransportCommand::Clock(command) => Clock(command),
_ => return None,
},
SequencerFocus::PhraseEditor => Editor(
PhraseCommand::input_to_command(&state.editor, input)?
),
SequencerFocus::PhraseList => match input.event() {
key!(Enter) => Enqueue(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
)),
_ => Phrases(
PhrasesCommand::input_to_command(&state.phrases, input)?
),
}
_ => return None
}
})
}

View file

@ -1,7 +1,88 @@
use crate::*;
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
use FileBrowserCommand::*;
use super::model_phrase_list::PhrasesMode::{Import, Export};
use super::phrase_list::PhrasesMode::{Import, Export};
/// Browses for phrase to import/export
#[derive(Debug, Clone)]
pub struct FileBrowser {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Measure<Tui>
}
render!(|self: FileBrowser|{
Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})
});
impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self {
cwd,
dirs,
files,
filter: "".to_string(),
index: 0,
scroll: 0,
size: Measure::new(),
})
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
pub fn chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
}
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]

View file

@ -1,192 +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])
}
fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
}
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
}
}
#[derive(Default, Debug, Clone)]
pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<RwLock<String>>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
/// Identifying color of scene
pub(crate) 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
}
}
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()),
player: PhrasePlayerModel::from(&self.clock),
};
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
pub(crate) name: Arc<RwLock<String>>,
/// Preferred width of track column
pub(crate) width: usize,
/// Identifying color of track
pub(crate) color: ItemColor,
/// MIDI player state
pub(crate) player: PhrasePlayerModel,
}
impl HasPlayer for ArrangerTrack {
fn player (&self) -> &impl MidiPlayerApi {
&self.player
}
fn player_mut (&mut self) -> &mut impl MidiPlayerApi {
&mut self.player
}
}
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
}
}
#[derive(PartialEq, Clone, Copy, Debug)]
/// Represents the current user selection in the arranger
pub enum ArrangerSelection {
/// The whole mix is selected
Mix,
/// A track is selected.
Track(usize),
/// A scene is selected.
Scene(usize),
/// A clip (track × scene) is selected.
Clip(usize, usize),
}
/// Focus identification methods
impl ArrangerSelection {
pub fn description <E: Engine> (
&self,
tracks: &Vec<ArrangerTrack>,
scenes: &Vec<ArrangerScene>,
) -> String {
format!("Selected: {}", match self {
Self::Mix => format!("Everything"),
Self::Track(t) => match tracks.get(*t) {
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
None => format!("T??"),
},
Self::Scene(s) => match scenes.get(*s) {
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
None => format!("S??"),
},
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
(Some(_), Some(scene)) => match scene.clip(*t) {
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
None => format!("T{t} S{s}: Empty")
},
_ => format!("T{t} S{s}: Empty"),
}
})
}
pub fn is_mix (&self) -> bool {
match self { Self::Mix => true, _ => false }
}
pub fn is_track (&self) -> bool {
match self { Self::Track(_) => true, _ => false }
}
pub fn is_scene (&self) -> bool {
match self { Self::Scene(_) => true, _ => false }
}
pub fn is_clip (&self) -> bool {
match self { Self::Clip(_, _) => true, _ => false }
}
pub fn track (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(t, _) => Some(*t),
Track(t) => Some(*t),
_ => None
}
}
pub fn scene (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(_, s) => Some(*s),
Scene(s) => Some(*s),
_ => None
}
}
}

View file

@ -1,62 +0,0 @@
use crate::*;
/// Browses for phrase to import/export
#[derive(Debug, Clone)]
pub struct FileBrowser {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Measure<Tui>
}
impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self {
cwd,
dirs,
files,
filter: "".to_string(),
index: 0,
scroll: 0,
size: Measure::new(),
})
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
pub fn chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
}

View file

@ -1,134 +0,0 @@
use crate::*;
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub(crate) note_len: usize,
/// The full piano roll is rendered to this buffer
pub(crate) buffer: BigBuffer,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub(crate) now: Arc<Pulse>,
/// Width and height of notes area at last render
pub(crate) size: Measure<Tui>,
pub(crate) note_lo: AtomicUsize,
pub(crate) note_point: AtomicUsize,
pub(crate) time_start: AtomicUsize,
pub(crate) time_point: AtomicUsize,
pub(crate) time_scale: AtomicUsize,
pub(crate) edit_mode: PhraseEditMode,
pub(crate) view_mode: PhraseViewMode,
}
impl std::fmt::Debug for PhraseEditorModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PhraseEditorModel")
.field("note_axis", &format!("{} {}",
self.note_lo.load(Ordering::Relaxed),
self.note_point.load(Ordering::Relaxed),
))
.field("time_axis", &format!("{} {} {}",
self.time_start.load(Ordering::Relaxed),
self.time_point.load(Ordering::Relaxed),
self.time_scale.load(Ordering::Relaxed),
))
.finish()
}
}
impl Default for PhraseEditorModel {
fn default () -> Self {
Self {
phrase: None,
note_len: 24,
buffer: Default::default(),
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
now: Pulse::default().into(),
size: Measure::new(),
edit_mode: PhraseEditMode::Scroll,
note_lo: 0.into(),
note_point: 0.into(),
time_start: 0.into(),
time_point: 0.into(),
time_scale: 24.into(),
view_mode: PhraseViewMode::PianoHorizontal {
time_zoom: 24,
note_zoom: PhraseViewNoteZoom::N(1)
},
}
}
}
impl PhraseEditorModel {
/// Put note at current position
pub fn put_note (&mut self) {
if let Some(phrase) = &self.phrase {
let time = self.time_point.load(Ordering::Relaxed);
let note = self.note_point.load(Ordering::Relaxed);
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from((127 - note) as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = self.view_mode.draw(&phrase);
}
}
/// Move time cursor forward by current note length
pub fn time_cursor_advance (&self) {
let point = self.time_point.load(Ordering::Relaxed);
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length;
self.time_point.store(forward(point), Ordering::Relaxed);
}
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
if phrase.is_some() {
self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap());
self.phrase = phrase;
} else {
self.buffer = Default::default();
self.phrase = None;
}
}
}
pub trait HasEditor {
fn editor (&self) -> &PhraseEditorModel;
fn editor_focused (&self) -> bool;
fn editor_entered (&self) -> bool;
}
impl HasEditor for SequencerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == SequencerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}
impl HasEditor for ArrangerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == ArrangerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}

View file

@ -1,65 +0,0 @@
use crate::*;
/// 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 { 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())
}
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone, Debug)]
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,90 +0,0 @@
use crate::*;
use super::*;
#[derive(Debug)]
pub struct PhraseListModel {
/// Collection of phrases
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Scroll offset
pub(crate) scroll: usize,
/// Mode switch
pub(crate) mode: Option<PhrasesMode>,
}
impl Default for PhraseListModel {
fn default () -> Self {
Self {
phrases: vec![RwLock::new(Phrase::default()).into()],
phrase: 0.into(),
scroll: 0,
mode: None,
}
}
}
impl PhraseListModel {
pub(crate) fn phrase_index (&self) -> usize {
self.phrase.load(Ordering::Relaxed)
}
pub(crate) fn set_phrase_index (&self, value: usize) {
self.phrase.store(value, Ordering::Relaxed);
}
pub(crate) fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.mode
}
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode> {
&mut self.mode
}
}
/// Modes for phrase pool
#[derive(Debug, Clone)]
pub enum PhrasesMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
Import(usize, FileBrowser),
/// Save phrase to disk
Export(usize, FileBrowser),
}
pub trait HasPhraseList: HasPhrases {
fn phrases_focused (&self) -> bool;
fn phrases_entered (&self) -> bool;
fn phrases_mode (&self) -> &Option<PhrasesMode>;
fn phrase_index (&self) -> usize;
}
impl HasPhraseList for SequencerTui {
fn phrases_focused (&self) -> bool {
self.focused() == SequencerFocus::PhraseList
}
fn phrases_entered (&self) -> bool {
self.entered() && self.phrases_focused()
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
impl HasPhraseList for ArrangerTui {
fn phrases_focused (&self) -> bool {
self.focused() == ArrangerFocus::Phrases
}
fn phrases_entered (&self) -> bool {
self.entered() && self.phrases_focused()
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}

View file

@ -1,5 +1,138 @@
use crate::*;
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub(crate) note_len: usize,
/// The full piano roll is rendered to this buffer
pub(crate) buffer: BigBuffer,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub(crate) now: Arc<Pulse>,
/// Width and height of notes area at last render
pub(crate) size: Measure<Tui>,
pub(crate) note_lo: AtomicUsize,
pub(crate) note_point: AtomicUsize,
pub(crate) time_start: AtomicUsize,
pub(crate) time_point: AtomicUsize,
pub(crate) time_scale: AtomicUsize,
pub(crate) edit_mode: PhraseEditMode,
pub(crate) view_mode: PhraseViewMode,
}
impl std::fmt::Debug for PhraseEditorModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PhraseEditorModel")
.field("note_axis", &format!("{} {}",
self.note_lo.load(Ordering::Relaxed),
self.note_point.load(Ordering::Relaxed),
))
.field("time_axis", &format!("{} {} {}",
self.time_start.load(Ordering::Relaxed),
self.time_point.load(Ordering::Relaxed),
self.time_scale.load(Ordering::Relaxed),
))
.finish()
}
}
impl Default for PhraseEditorModel {
fn default () -> Self {
Self {
phrase: None,
note_len: 24,
buffer: Default::default(),
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
now: Pulse::default().into(),
size: Measure::new(),
edit_mode: PhraseEditMode::Scroll,
note_lo: 0.into(),
note_point: 0.into(),
time_start: 0.into(),
time_point: 0.into(),
time_scale: 24.into(),
view_mode: PhraseViewMode::PianoHorizontal {
time_zoom: 24,
note_zoom: PhraseViewNoteZoom::N(1)
},
}
}
}
impl PhraseEditorModel {
/// Put note at current position
pub fn put_note (&mut self) {
if let Some(phrase) = &self.phrase {
let time = self.time_point.load(Ordering::Relaxed);
let note = self.note_point.load(Ordering::Relaxed);
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from((127 - note) as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = self.view_mode.draw(&phrase);
}
}
/// Move time cursor forward by current note length
pub fn time_cursor_advance (&self) {
let point = self.time_point.load(Ordering::Relaxed);
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length;
self.time_point.store(forward(point), Ordering::Relaxed);
}
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show_phrase (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
if phrase.is_some() {
self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap());
self.phrase = phrase;
} else {
self.buffer = Default::default();
self.phrase = None;
}
}
}
pub trait HasEditor {
fn editor (&self) -> &PhraseEditorModel;
fn editor_focused (&self) -> bool;
fn editor_entered (&self) -> bool;
}
impl HasEditor for SequencerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == SequencerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}
impl HasEditor for ArrangerTui {
fn editor (&self) -> &PhraseEditorModel {
&self.editor
}
fn editor_focused (&self) -> bool {
self.focused() == ArrangerFocus::PhraseEditor
}
fn editor_entered (&self) -> bool {
self.entered() && self.editor_focused()
}
}
pub struct PhraseView<'a> {
focused: bool,
entered: bool,
@ -473,3 +606,131 @@ impl PhraseViewMode {
//////"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
////];
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
AppendNote,
PutNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
Show(Option<Arc<RwLock<Phrase>>>),
SetEditMode(PhraseEditMode),
ToggleDirection,
}
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
use PhraseCommand::*;
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
let note_lo = state.note_lo.load(Ordering::Relaxed);
let note_point = state.note_point.load(Ordering::Relaxed);
let time_start = state.time_start.load(Ordering::Relaxed);
let time_point = state.time_point.load(Ordering::Relaxed);
let time_zoom = state.view_mode.time_zoom();
let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('a')) => AppendNote,
key!(Char('s')) => PutNote,
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
key!(Char('n')) => { todo!("toggle keys vs notes") },
_ => match state.edit_mode {
PhraseEditMode::Scroll => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
key!(Up) => SetNoteScroll(note_lo + 1),
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
key!(PageUp) => SetNoteScroll(note_lo + 3),
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
key!(Right) => SetTimeScroll(time_start + 1),
_ => return None
},
PhraseEditMode::Note => match from.event() {
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
key!(Up) => SetNoteCursor(note_point + 1),
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key!(PageUp) => SetNoteCursor(note_point + 3),
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
key!(Right) => SetTimeCursor((time_point + time_zoom) % length),
_ => return None
},
}
})
}
}
impl Command<PhraseEditorModel> for PhraseCommand {
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
use PhraseCommand::*;
Ok(match self {
Show(phrase) => {
state.show_phrase(phrase);
None
},
ToggleDirection => {
todo!()
},
SetEditMode(mode) => {
state.edit_mode = mode;
None
}
AppendNote => {
state.put_note();
state.time_cursor_advance();
None
},
PutNote => {
state.put_note();
None
},
SetTimeCursor(time) => {
state.time_point.store(time, Ordering::Relaxed);
None
},
SetTimeScroll(time) => {
state.time_start.store(time, Ordering::Relaxed);
None
},
SetTimeZoom(zoom) => {
state.view_mode.set_time_zoom(zoom);
state.show_phrase(state.phrase.clone());
None
},
SetNoteScroll(note) => {
state.note_lo.store(note, Ordering::Relaxed);
None
},
SetNoteLength(time) => {
state.note_len = time;
None
},
SetNoteCursor(note) => {
let note = 127.min(note);
let start = state.note_lo.load(Ordering::Relaxed);
state.note_point.store(note, Ordering::Relaxed);
if note < start {
state.note_lo.store(note, Ordering::Relaxed);
}
None
},
})
}
}
#[derive(Copy, Clone, Debug)]
pub enum PhraseEditMode {
Note,
Scroll,
}

View file

@ -0,0 +1,129 @@
use crate::*;
use super::phrase_list::{PhraseListModel, PhrasesMode};
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
/// 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 { 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())
}
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone, Debug)]
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,
}
}
}
render!(|self: PhraseLength|{
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
row!(move|add|match self.focus {
None =>
add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])),
Some(PhraseLengthFocus::Bar) =>
add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])),
Some(PhraseLengthFocus::Beat) =>
add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])),
Some(PhraseLengthFocus::Tick) =>
add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])),
})
});
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
impl Command<PhraseListModel> for PhraseLengthCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
match state.phrases_mode_mut().clone() {
Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self {
Cancel => { *state.phrases_mode_mut() = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let mut phrase = state.phrases()[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
std::mem::drop(phrase);
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
},
_ => unreachable!()
};
Ok(None)
}
}

View file

@ -0,0 +1,303 @@
use super::*;
use crate::{
*,
api::PhrasePoolCommand as Pool,
tui::{
phrase_rename::PhraseRenameCommand as Rename,
phrase_length::PhraseLengthCommand as Length,
file_browser::FileBrowserCommand as Browse,
}
};
#[derive(Debug)]
pub struct PhraseListModel {
/// Collection of phrases
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Scroll offset
pub(crate) scroll: usize,
/// Mode switch
pub(crate) mode: Option<PhrasesMode>,
}
impl Default for PhraseListModel {
fn default () -> Self {
Self {
phrases: vec![RwLock::new(Phrase::default()).into()],
phrase: 0.into(),
scroll: 0,
mode: None,
}
}
}
impl PhraseListModel {
pub(crate) fn phrase_index (&self) -> usize {
self.phrase.load(Ordering::Relaxed)
}
pub(crate) fn set_phrase_index (&self, value: usize) {
self.phrase.store(value, Ordering::Relaxed);
}
pub(crate) fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.mode
}
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PhrasesMode> {
&mut self.mode
}
}
/// Modes for phrase pool
#[derive(Debug, Clone)]
pub enum PhrasesMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
Import(usize, FileBrowser),
/// Save phrase to disk
Export(usize, FileBrowser),
}
pub trait HasPhraseList: HasPhrases {
fn phrases_focused (&self) -> bool;
fn phrases_entered (&self) -> bool;
fn phrases_mode (&self) -> &Option<PhrasesMode>;
fn phrase_index (&self) -> usize;
}
impl HasPhraseList for SequencerTui {
fn phrases_focused (&self) -> bool {
self.focused() == SequencerFocus::PhraseList
}
fn phrases_entered (&self) -> bool {
self.entered() && self.phrases_focused()
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
impl HasPhraseList for ArrangerTui {
fn phrases_focused (&self) -> bool {
self.focused() == ArrangerFocus::Phrases
}
fn phrases_entered (&self) -> bool {
self.entered() && self.phrases_focused()
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
pub struct PhraseListView<'a> {
pub(crate) title: &'static str,
pub(crate) focused: bool,
pub(crate) entered: bool,
pub(crate) phrases: &'a Vec<Arc<RwLock<Phrase>>>,
pub(crate) index: usize,
pub(crate) mode: &'a Option<PhrasesMode>
}
impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> {
fn from (state: &'a T) -> Self {
Self {
title: "Phrases",
focused: state.phrases_focused(),
entered: state.phrases_entered(),
phrases: state.phrases(),
index: state.phrase_index(),
mode: state.phrases_mode(),
}
}
}
// TODO: Display phrases always in order of appearance
render!(|self: PhraseListView<'a>|{
let Self { title, focused, entered, phrases, index, mode } = self;
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 upper_left = format!("[{}] {title}", if *entered {""} else {" "});
let upper_right = format!("({})", phrases.len());
lay!([
Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color))
.wrap(Tui::bg(Color::Rgb(28, 35, 25), Tui::fill_xy(col!(move|add|match mode {
Some(PhrasesMode::Import(_, ref browser)) => {
add(browser)
},
Some(PhrasesMode::Export(_, ref browser)) => {
add(browser)
},
_ => {
for (i, phrase) in phrases.iter().enumerate() {
add(&lay!(|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 = Tui::fill_x(Tui::at_e(length));
let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length]));
let mut row2 = format!(" {name}");
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
if *focused && i == *phrase {
row2 = format!("{row2}");
}
};
let row2 = Tui::bold(true, row2);
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?;
if *entered && i == *index {
add(&CORNERS)?;
}
Ok(())
}))?;
}
Ok(())
}
})))),
Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))),
Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))),
])
});
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
Select(usize),
Phrase(Pool),
Rename(Rename),
Length(Length),
Import(Browse),
Export(Browse),
}
impl Command<PhraseListModel> for PhrasesCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
use PhrasesCommand::*;
Ok(match self {
Phrase(command) => command.execute(state)?.map(Phrase),
Rename(command) => match command {
PhraseRenameCommand::Begin => {
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
*state.phrases_mode_mut() = Some(
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
);
None
},
_ => command.execute(state)?.map(Rename)
},
Length(command) => match command {
PhraseLengthCommand::Begin => {
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
*state.phrases_mode_mut() = Some(
PhrasesMode::Rename(state.phrase_index(), name)
);
None
},
_ => command.execute(state)?.map(Length)
},
Import(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhrasesMode::Import(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Import)
},
Export(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhrasesMode::Export(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Export)
},
Select(phrase) => {
state.set_phrase_index(phrase);
None
},
})
}
}
impl HasPhrases for PhraseListModel {
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self.phrases
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
&mut self.phrases
}
}
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
Some(match state.phrases_mode() {
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
})
}
}
fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PhrasesCommand as Cmd;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key!(Char('n')) => Cmd::Rename(Rename::Begin),
key!(Char('t')) => Cmd::Length(Length::Begin),
key!(Char('m')) => Cmd::Import(Browse::Begin),
key!(Char('x')) => Cmd::Export(Browse::Begin),
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
key!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
key!(Char(',')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index))
} else {
return None
},
key!(Char('.')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(Pool::Swap(index + 1, index))
} else {
return None
},
key!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(Pool::Delete(index))
} else {
return None
},
key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
))),
key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
))),
key!(Char('d')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
Cmd::Phrase(Pool::Add(index + 1, phrase))
},
_ => return None
})
}

View file

@ -1,21 +0,0 @@
use crate::*;
render!(|self: FileBrowser|{
Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&Tui::bold(i == self.index, name.as_str()))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})
});

View file

@ -1,17 +0,0 @@
use crate::*;
render!(|self: PhraseLength|{
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
row!(move|add|match self.focus {
None =>
add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])),
Some(PhraseLengthFocus::Bar) =>
add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])),
Some(PhraseLengthFocus::Beat) =>
add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])),
Some(PhraseLengthFocus::Tick) =>
add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])),
})
});

View file

@ -1,74 +0,0 @@
use crate::*;
pub struct PhraseListView<'a> {
pub(crate) title: &'static str,
pub(crate) focused: bool,
pub(crate) entered: bool,
pub(crate) phrases: &'a Vec<Arc<RwLock<Phrase>>>,
pub(crate) index: usize,
pub(crate) mode: &'a Option<PhrasesMode>
}
impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> {
fn from (state: &'a T) -> Self {
Self {
title: "Phrases",
focused: state.phrases_focused(),
entered: state.phrases_entered(),
phrases: state.phrases(),
index: state.phrase_index(),
mode: state.phrases_mode(),
}
}
}
// TODO: Display phrases always in order of appearance
render!(|self: PhraseListView<'a>|{
let Self { title, focused, entered, phrases, index, mode } = self;
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 upper_left = format!("[{}] {title}", if *entered {""} else {" "});
let upper_right = format!("({})", phrases.len());
lay!([
Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color))
.wrap(Tui::bg(Color::Rgb(28, 35, 25), Tui::fill_xy(col!(move|add|match mode {
Some(PhrasesMode::Import(_, ref browser)) => {
add(browser)
},
Some(PhrasesMode::Export(_, ref browser)) => {
add(browser)
},
_ => {
for (i, phrase) in phrases.iter().enumerate() {
add(&lay!(|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 = Tui::fill_x(Tui::at_e(length));
let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length]));
let mut row2 = format!(" {name}");
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
if *focused && i == *phrase {
row2 = format!("{row2}");
}
};
let row2 = Tui::bold(true, row2);
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?;
if *entered && i == *index {
add(&CORNERS)?;
}
Ok(())
}))?;
}
Ok(())
}
})))),
Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))),
Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))),
])
});