mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 20:26:42 +01:00
rename phrase -> clip mostly everywhere
This commit is contained in:
parent
709391ff0a
commit
08f7a62692
24 changed files with 426 additions and 423 deletions
|
|
@ -42,15 +42,15 @@ audio!(|self: Arranger, client, scope|{
|
|||
// FIXME: one of these per playing track
|
||||
//self.now.set(0.);
|
||||
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
//let phrase = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
||||
//if let Some(Some(Some(phrase))) = phrase {
|
||||
//let clip = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
||||
//if let Some(Some(Some(clip))) = clip {
|
||||
//if let Some(track) = self.tracks().get(t) {
|
||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_clip {
|
||||
//let clip = clip.read().unwrap();
|
||||
//if *playing.read().unwrap() == *clip {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//let now = (pulse - start) % clip.length as f64;
|
||||
//self.now.set(now);
|
||||
//}
|
||||
//}
|
||||
|
|
@ -62,7 +62,7 @@ audio!(|self: Arranger, client, scope|{
|
|||
return Control::Continue
|
||||
});
|
||||
has_clock!(|self: Arranger|&self.clock);
|
||||
has_phrases!(|self: Arranger|self.pool.phrases);
|
||||
has_clips!(|self: Arranger|self.pool.clips);
|
||||
has_editor!(|self: Arranger|self.editor);
|
||||
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
|
||||
impl Arranger {
|
||||
|
|
@ -75,26 +75,26 @@ impl Arranger {
|
|||
pub 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());
|
||||
let clip = self.scenes[s].clips[t].clone();
|
||||
if track.player.play_clip.is_some() || clip.is_some() {
|
||||
track.player.enqueue_next(clip.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[t].player.enqueue_next(phrase.as_ref());
|
||||
let clip = self.scenes[s].clips[t].clone();
|
||||
self.tracks[t].player.enqueue_next(clip.as_ref());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub fn selected_phrase (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
pub fn selected_clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
||||
}
|
||||
pub fn toggle_loop (&mut self) {
|
||||
if let Some(phrase) = self.selected_phrase() {
|
||||
phrase.write().unwrap().toggle_loop()
|
||||
if let Some(clip) = self.selected_clip() {
|
||||
clip.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
pub fn randomize_color (&mut self) {
|
||||
|
|
@ -102,8 +102,8 @@ impl Arranger {
|
|||
ArrangerSelection::Mix => { self.color = ItemPalette::random() },
|
||||
ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
||||
ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
||||
ArrangerSelection::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
|
||||
phrase.write().unwrap().color = ItemPalette::random();
|
||||
ArrangerSelection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
|
||||
clip.write().unwrap().color = ItemPalette::random();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
|||
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
||||
// Transport: Play from start or rewind to start
|
||||
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
||||
key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(Some(state.pool.phrase().clone()))),
|
||||
key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(Some(state.pool.clip().clone()))),
|
||||
ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add),
|
||||
ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add),
|
||||
// Tab: Toggle visibility of phrase pool column
|
||||
// Tab: Toggle visibility of clip pool column
|
||||
key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)),
|
||||
}, {
|
||||
use ArrangerSelection as Selected;
|
||||
|
|
@ -81,7 +81,7 @@ keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
|
|||
kpat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
kpat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
kpat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
kpat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.phrase().clone())))),
|
||||
kpat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.clip().clone())))),
|
||||
kpat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
|
||||
kpat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
|
||||
|
|
@ -178,16 +178,16 @@ command!(|self: ArrangerCommand, state: Arranger|match self {
|
|||
},
|
||||
Self::Phrases(cmd) => {
|
||||
match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
// autoselect: automatically load selected clip in editor
|
||||
PoolCommand::Select(_) => {
|
||||
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
state.editor.set_clip(Some(state.pool.clip()));
|
||||
undo
|
||||
},
|
||||
// reload phrase in editor to update color
|
||||
// reload clip in editor to update color
|
||||
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
|
||||
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
state.editor.set_clip(Some(state.pool.clip()));
|
||||
undo
|
||||
},
|
||||
_ => cmd.delegate(&mut state.pool, Self::Phrases)?
|
||||
|
|
@ -234,9 +234,9 @@ command!(|self: ArrangerSceneCommand, state: Arranger|match self {
|
|||
});
|
||||
command!(|self: ArrangerClipCommand, state: Arranger|match self {
|
||||
Self::Get(track, scene) => { todo!() },
|
||||
Self::Put(track, scene, phrase) => {
|
||||
Self::Put(track, scene, clip) => {
|
||||
let old = state.scenes[scene].clips[track].clone();
|
||||
state.scenes[scene].clips[track] = phrase;
|
||||
state.scenes[scene].clips[track] = clip;
|
||||
Some(Self::Put(track, scene, old))
|
||||
},
|
||||
Self::Enqueue(track, scene) => {
|
||||
|
|
|
|||
|
|
@ -56,22 +56,22 @@ impl ArrangerScene {
|
|||
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
/// Returns the pulse length of the longest phrase in the scene
|
||||
/// Returns the pulse length of the longest clip in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips().iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// Returns true if all phrases in the scene are
|
||||
/// Returns true if all clips in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
||||
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(clip) => tracks
|
||||
Some(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(phrase))) = track.player().play_phrase() {
|
||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
||||
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||
*clip.read().unwrap() == *c.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ impl Arranger {
|
|||
let color: ItemPalette = track.color().dark.into();
|
||||
let timebase = self.clock().timebase();
|
||||
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
|
||||
if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
if let Some((_, Some(clip))) = track.player.play_clip().as_ref() {
|
||||
let length = clip.read().unwrap().length;
|
||||
let elapsed = track.player.pulses_since_start().unwrap() as usize;
|
||||
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
|
||||
} else {
|
||||
|
|
@ -229,7 +229,7 @@ impl Arranger {
|
|||
{
|
||||
let timebase = ¤t.timebase;
|
||||
let mut result = String::new();
|
||||
if let Some((t, _)) = track.player.next_phrase().as_ref() {
|
||||
if let Some((t, _)) = track.player.next_clip().as_ref() {
|
||||
let target = t.pulse.get();
|
||||
let current = current.pulse.get();
|
||||
if target > current {
|
||||
|
|
@ -292,15 +292,15 @@ impl Arranger {
|
|||
fn cell_clip <'a> (
|
||||
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
||||
) -> impl Content<TuiOut> + use<'a> {
|
||||
scene.clips.get(index).map(|clip|clip.as_ref().map(|phrase|{
|
||||
let phrase = phrase.read().unwrap();
|
||||
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
||||
let clip = clip.read().unwrap();
|
||||
let mut bg = TuiTheme::border_bg();
|
||||
let name = phrase.name.to_string();
|
||||
let name = clip.name.to_string();
|
||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||
let color = phrase.color;
|
||||
let color = clip.color;
|
||||
bg = color.dark.rgb;
|
||||
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
||||
if *playing.read().unwrap() == *phrase {
|
||||
if let Some((_, Some(ref playing))) = track.player.play_clip() {
|
||||
if *playing.read().unwrap() == *clip {
|
||||
bg = color.light.rgb
|
||||
}
|
||||
};
|
||||
|
|
@ -312,8 +312,8 @@ impl Arranger {
|
|||
}
|
||||
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let w = self.size.w();
|
||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||
let pool_w = if self.pool.visible { phrase_w } else { 0 };
|
||||
let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||
let pool_w = if self.pool.visible { clip_w } else { 0 };
|
||||
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
||||
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,21 +15,21 @@ pub enum GrooveboxCommand {
|
|||
}
|
||||
|
||||
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
Self::Enqueue(clip) => {
|
||||
state.player.enqueue_next(clip.as_ref());
|
||||
None
|
||||
},
|
||||
Self::Pool(cmd) => match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
// autoselect: automatically load selected clip in editor
|
||||
PoolCommand::Select(_) => {
|
||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
state.editor.set_clip(Some(state.pool.clip()));
|
||||
undo
|
||||
},
|
||||
// update color in all places simultaneously
|
||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
state.editor.set_clip(Some(state.pool.clip()));
|
||||
undo
|
||||
},
|
||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||
|
|
@ -52,10 +52,10 @@ handle!(TuiIn: |self: Groovebox, input|
|
|||
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
|
||||
// Tab: Toggle compact mode
|
||||
key(Tab) => Cmd::Compact(!state.compact),
|
||||
// q: Enqueue currently edited phrase
|
||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
||||
// 0: Enqueue phrase 0 (stop all)
|
||||
key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
|
||||
// q: Enqueue currently edited clip
|
||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.clip().clone())),
|
||||
// 0: Enqueue clip 0 (stop all)
|
||||
key(Char('0')) => Cmd::Enqueue(Some(state.pool.clips()[0].clone())),
|
||||
// TODO: k: toggle on-screen keyboard
|
||||
ctrl(key(Char('k'))) => todo!("keyboard"),
|
||||
// Transport: Play from start or rewind to start
|
||||
|
|
@ -72,10 +72,10 @@ keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand
|
|||
shift(key(Delete)) => Cmd::Sampler(
|
||||
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
|
||||
),
|
||||
// e: Toggle between editing currently playing or other phrase
|
||||
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.pool.phrase().clone();
|
||||
// e: Toggle between editing currently playing or other clip
|
||||
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
||||
let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.pool.clip().clone();
|
||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
selected
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
mod pool_tui; pub use self::pool_tui::*;
|
||||
mod clip_length; pub use self::clip_length::*;
|
||||
mod clip_rename; pub use self::clip_rename::*;
|
||||
mod clip_select; pub use self::clip_select::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolModel {
|
||||
pub(crate) visible: bool,
|
||||
/// Collection of phrases
|
||||
pub(crate) phrases: Vec<Arc<RwLock<MidiClip>>>,
|
||||
/// Selected phrase
|
||||
pub(crate) phrase: AtomicUsize,
|
||||
/// Collection of clips
|
||||
pub(crate) clips: Vec<Arc<RwLock<MidiClip>>>,
|
||||
/// Selected clip
|
||||
pub(crate) clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PoolMode>,
|
||||
/// Rendered size
|
||||
|
|
@ -20,29 +19,29 @@ pub struct PoolModel {
|
|||
scroll: usize,
|
||||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, PhraseLengthFocus),
|
||||
/// Load phrase from disk
|
||||
/// Load clip from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save phrase to disk
|
||||
/// Save clip to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PoolCommand {
|
||||
Show(bool),
|
||||
/// Update the contents of the phrase pool
|
||||
/// Update the contents of the clip pool
|
||||
Phrase(MidiPoolCommand),
|
||||
/// Select a phrase from the phrase pool
|
||||
/// Select a clip from the clip pool
|
||||
Select(usize),
|
||||
/// Rename a phrase
|
||||
/// Rename a clip
|
||||
Rename(PhraseRenameCommand),
|
||||
/// Change the length of a phrase
|
||||
/// Change the length of a clip
|
||||
Length(PhraseLengthCommand),
|
||||
/// Import from file
|
||||
Import(FileBrowserCommand),
|
||||
|
|
@ -59,9 +58,9 @@ command!(|self:PoolCommand, state: PoolModel|{
|
|||
}
|
||||
Rename(command) => match command {
|
||||
PhraseRenameCommand::Begin => {
|
||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
||||
let length = state.clips()[state.clip_index()].read().unwrap().length;
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Length(state.clip_index(), length, PhraseLengthFocus::Bar)
|
||||
);
|
||||
None
|
||||
},
|
||||
|
|
@ -69,9 +68,9 @@ command!(|self:PoolCommand, state: PoolModel|{
|
|||
},
|
||||
Length(command) => match command {
|
||||
PhraseLengthCommand::Begin => {
|
||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Rename(state.phrase_index(), name)
|
||||
let name = state.clips()[state.clip_index()].read().unwrap().name.clone();
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Rename(state.clip_index(), name)
|
||||
);
|
||||
None
|
||||
},
|
||||
|
|
@ -79,8 +78,8 @@ command!(|self:PoolCommand, state: PoolModel|{
|
|||
},
|
||||
Import(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?)
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Import(state.clip_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
|
|
@ -88,34 +87,34 @@ command!(|self:PoolCommand, state: PoolModel|{
|
|||
},
|
||||
Export(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?)
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Export(state.clip_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(phrase) => {
|
||||
state.set_phrase_index(phrase);
|
||||
Select(clip) => {
|
||||
state.set_clip_index(clip);
|
||||
None
|
||||
},
|
||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||
}
|
||||
});
|
||||
|
||||
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.phrases_mode() {
|
||||
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() {
|
||||
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
||||
_ => to_phrases_command(state, input)?
|
||||
_ => to_clips_command(state, input)?
|
||||
});
|
||||
|
||||
fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||
fn to_clips_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PoolCommand as Cmd;
|
||||
let index = state.phrase_index();
|
||||
let count = state.phrases().len();
|
||||
let index = state.clip_index();
|
||||
let count = state.clips().len();
|
||||
Some(match input {
|
||||
kpat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
||||
kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
||||
|
|
@ -123,25 +122,25 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand>
|
|||
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||
kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
||||
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
|
||||
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||
),
|
||||
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||
index.saturating_add(1) % state.phrases().len()
|
||||
index.saturating_add(1) % state.clips().len()
|
||||
),
|
||||
kpat!(Char('<')) => if index > 1 {
|
||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
||||
state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||
Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||
state.set_phrase_index(state.phrase_index() + 1);
|
||||
state.set_clip_index(state.clip_index() + 1);
|
||||
Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Delete) => if index > 0 {
|
||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
||||
state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||
Cmd::Phrase(MidiPoolCommand::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
|
|
@ -153,9 +152,9 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand>
|
|||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
|
||||
Cmd::Phrase(MidiPoolCommand::Add(index + 1, phrase))
|
||||
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
Cmd::Phrase(MidiPoolCommand::Add(index + 1, clip))
|
||||
},
|
||||
_ => return None
|
||||
})
|
||||
|
|
@ -164,33 +163,33 @@ impl Default for PoolModel {
|
|||
fn default () -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
phrases: vec![RwLock::new(MidiClip::default()).into()],
|
||||
phrase: 0.into(),
|
||||
clips: vec![RwLock::new(MidiClip::default()).into()],
|
||||
clip: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
size: Measure::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
from!(|phrase:&Arc<RwLock<MidiClip>>|PoolModel = {
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|PoolModel = {
|
||||
let mut model = Self::default();
|
||||
model.phrases.push(phrase.clone());
|
||||
model.phrase.store(1, Relaxed);
|
||||
model.clips.push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
has_phrases!(|self: PoolModel|self.phrases);
|
||||
has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]);
|
||||
has_clips!(|self: PoolModel|self.clips);
|
||||
has_clip!(|self: PoolModel|self.clips[self.clip_index()]);
|
||||
impl PoolModel {
|
||||
pub(crate) fn phrase_index (&self) -> usize {
|
||||
self.phrase.load(Relaxed)
|
||||
pub(crate) fn clip_index (&self) -> usize {
|
||||
self.clip.load(Relaxed)
|
||||
}
|
||||
pub(crate) fn set_phrase_index (&self, value: usize) {
|
||||
self.phrase.store(value, Relaxed);
|
||||
pub(crate) fn set_clip_index (&self, value: usize) {
|
||||
self.clip.store(value, Relaxed);
|
||||
}
|
||||
pub(crate) fn phrases_mode (&self) -> &Option<PoolMode> {
|
||||
pub(crate) fn clips_mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
pub(crate) fn clips_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
pub fn file_picker (&self) -> Option<&FileBrowser> {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ use PhraseLengthFocus::*;
|
|||
use PhraseLengthCommand::*;
|
||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
|
|
@ -97,9 +97,9 @@ pub enum PhraseLengthCommand {
|
|||
}
|
||||
|
||||
command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PoolMode::Length(phrase, ref mut length, ref mut focus)) => match self {
|
||||
Cancel => { *state.phrases_mode_mut() = None; },
|
||||
match state.clips_mode_mut().clone() {
|
||||
Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self {
|
||||
Cancel => { *state.clips_mode_mut() = None; },
|
||||
Prev => { focus.prev() },
|
||||
Next => { focus.next() },
|
||||
Inc => match focus {
|
||||
|
|
@ -113,11 +113,11 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
|
|||
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;
|
||||
let mut clip = state.clips()[clip].write().unwrap();
|
||||
let old_length = clip.length;
|
||||
clip.length = length;
|
||||
std::mem::drop(clip);
|
||||
*state.clips_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
|
|
@ -128,7 +128,7 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
|
|||
});
|
||||
|
||||
input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{
|
||||
if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() {
|
||||
if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() {
|
||||
match input {
|
||||
kpat!(Up) => Self::Inc,
|
||||
kpat!(Down) => Self::Dec,
|
||||
|
|
|
|||
|
|
@ -12,19 +12,19 @@ pub enum PhraseRenameCommand {
|
|||
impl Command<PoolModel> for PhraseRenameCommand {
|
||||
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PoolMode::Rename(phrase, ref mut old_name)) => match self {
|
||||
match state.clips_mode_mut().clone() {
|
||||
Some(PoolMode::Rename(clip, ref mut old_name)) => match self {
|
||||
Set(s) => {
|
||||
state.phrases()[phrase].write().unwrap().name = s;
|
||||
state.clips()[clip].write().unwrap().name = s;
|
||||
return Ok(Some(Self::Set(old_name.clone().into())))
|
||||
},
|
||||
Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
*state.phrases_mode_mut() = None;
|
||||
*state.clips_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
state.phrases()[phrase].write().unwrap().name = old_name.clone().into();
|
||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
|
|
@ -37,7 +37,7 @@ impl Command<PoolModel> for PhraseRenameCommand {
|
|||
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
|
||||
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
|
||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() {
|
||||
Some(match input {
|
||||
kpat!(Char(c)) => {
|
||||
let mut new_name = old_name.clone().to_string();
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
use crate::*;
|
||||
|
|
@ -3,14 +3,14 @@ use crate::*;
|
|||
pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
|
||||
render!(TuiOut: (self: PoolView<'a>) => {
|
||||
let Self(compact, model) = self;
|
||||
let PoolModel { phrases, mode, .. } = self.1;
|
||||
let color = self.1.phrase().read().unwrap().color;
|
||||
let PoolModel { clips, mode, .. } = self.1;
|
||||
let color = self.1.clip().read().unwrap().color;
|
||||
Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, Outer(
|
||||
Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)
|
||||
).enclose(Map::new(||model.phrases().iter(), |clip, i|{
|
||||
).enclose(Map::new(||model.clips().iter(), |clip, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.phrase_index();
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ impl Sequencer {
|
|||
}
|
||||
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let w = self.size.w();
|
||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||
let pool_w = if self.pool.visible { phrase_w } else { 0 };
|
||||
let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||
let pool_w = if self.pool.visible { clip_w } else { 0 };
|
||||
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
||||
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ impl Sequencer {
|
|||
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
|
||||
double(("a", "append"), ("s", "set note"),),
|
||||
double((",.", "length"), ("<>", "triplet"), ),
|
||||
double(("[]", "phrase"), ("{}", "order"), ),
|
||||
double(("[]", "clip"), ("{}", "order"), ),
|
||||
double(("q", "enqueue"), ("e", "edit"), ),
|
||||
double(("c", "color"), ("", ""),),
|
||||
))
|
||||
|
|
@ -95,7 +95,7 @@ audio!(|self:Sequencer, client, scope|{
|
|||
});
|
||||
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
||||
has_clock!(|self:Sequencer|&self.player.clock);
|
||||
has_phrases!(|self:Sequencer|self.pool.phrases);
|
||||
has_clips!(|self:Sequencer|self.pool.clips);
|
||||
has_editor!(|self:Sequencer|self.editor);
|
||||
handle!(TuiIn: |self:Sequencer,input|SequencerCommand::execute_with_state(self, input.event()));
|
||||
#[derive(Clone, Debug)] pub enum SequencerCommand {
|
||||
|
|
@ -119,14 +119,14 @@ keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
|||
key(Char('U')) => Cmd::History( 1),
|
||||
// Tab: Toggle compact mode
|
||||
key(Tab) => Cmd::Compact(!state.compact),
|
||||
// q: Enqueue currently edited phrase
|
||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
||||
// 0: Enqueue phrase 0 (stop all)
|
||||
key(Char('0')) => Cmd::Enqueue(Some(state.phrases()[0].clone())),
|
||||
// e: Toggle between editing currently playing or other phrase
|
||||
key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.pool.phrase().clone();
|
||||
// q: Enqueue currently edited clip
|
||||
key(Char('q')) => Cmd::Enqueue(Some(state.pool.clip().clone())),
|
||||
// 0: Enqueue clip 0 (stop all)
|
||||
key(Char('0')) => Cmd::Enqueue(Some(state.clips()[0].clone())),
|
||||
// e: Toggle between editing currently playing or other clip
|
||||
key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() {
|
||||
let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.pool.clip().clone();
|
||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
selected
|
||||
} else {
|
||||
|
|
@ -143,21 +143,21 @@ keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
|
|||
return None
|
||||
});
|
||||
command!(|self: SequencerCommand, state: Sequencer|match self {
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
Self::Enqueue(clip) => {
|
||||
state.player.enqueue_next(clip.as_ref());
|
||||
None
|
||||
},
|
||||
Self::Pool(cmd) => match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
// autoselect: automatically load selected clip in editor
|
||||
PoolCommand::Select(_) => {
|
||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
state.editor.set_clip(Some(state.pool.clip()));
|
||||
undo
|
||||
},
|
||||
// update color in all places simultaneously
|
||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
state.editor.set_clip(Some(state.pool.clip()));
|
||||
undo
|
||||
},
|
||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue