mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
move pool to tek_midi; implement some Default
This commit is contained in:
parent
bb52555183
commit
1aa0551931
17 changed files with 779 additions and 775 deletions
|
|
@ -141,7 +141,7 @@ pub fn main () -> Usually<()> {
|
|||
let mut player = default_player(jack, Some(&clip))?;
|
||||
player.clock = default_bpm(player.clock);
|
||||
let sampler = default_sampler(jack)?;
|
||||
jack.connect_ports(&player.midi_outs[0].port, &sampler.midi_in.port)?;
|
||||
jack.connect_ports(&player.midi_outs[0].port, &sampler.midi_in.as_ref().unwrap().port)?;
|
||||
App::groovebox(
|
||||
jack, (&clip).into(), (&clip).into(),
|
||||
Some(player), &midi_froms, &midi_tos,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub(crate) use ::tek_tui::{
|
|||
*,
|
||||
tek_input::*,
|
||||
tek_output::*,
|
||||
crossterm::event::KeyCode,
|
||||
crossterm::event::*,
|
||||
ratatui::style::{Style, Stylize, Color}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ pub struct MidiPlayer {
|
|||
/// State of clock and playhead
|
||||
pub clock: Clock,
|
||||
/// Start time and clip being played
|
||||
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Start time and next clip
|
||||
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
|
|
@ -45,6 +45,26 @@ pub struct MidiPlayer {
|
|||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
}
|
||||
impl Default for MidiPlayer {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
play_clip: None,
|
||||
next_clip: None,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
clock: Clock::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MidiPlayer {
|
||||
pub fn new (
|
||||
jack: &Arc<RwLock<JackConnection>>,
|
||||
|
|
@ -56,20 +76,11 @@ impl MidiPlayer {
|
|||
let name = name.as_ref();
|
||||
let clock = Clock::from(jack);
|
||||
Ok(Self {
|
||||
play_clip: Some((Moment::zero(&clock.timebase), clip.cloned())),
|
||||
next_clip: None,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
|
||||
midi_ins: vec![JackPort::<MidiIn>::new(jack, format!("M/{name}"), midi_from)?,],
|
||||
midi_outs: vec![JackPort::<MidiOut>::new(jack, format!("{name}/M"), midi_to)?, ],
|
||||
play_clip: Some((Moment::zero(&clock.timebase), clip.cloned())),
|
||||
clock,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
pub fn play_status (&self) -> impl Content<TuiOut> {
|
||||
|
|
|
|||
|
|
@ -102,3 +102,493 @@ impl<T: HasClips> Command<T> for MidiPoolCommand {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolModel {
|
||||
pub visible: bool,
|
||||
/// Collection of clips
|
||||
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
/// Rendered size
|
||||
size: Measure<TuiOut>,
|
||||
/// Scroll offset
|
||||
scroll: usize,
|
||||
}
|
||||
impl Default for PoolModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
clip: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
size: Measure::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|PoolModel = {
|
||||
let mut model = Self::default();
|
||||
model.clips.write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
|
||||
pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
|
||||
render!(TuiOut: (self: PoolView<'a>) => {
|
||||
let Self(compact, model) = self;
|
||||
let PoolModel { clips, mode, .. } = self.1;
|
||||
let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into());
|
||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||
let iter = | |model.clips().clone().into_iter();
|
||||
Tui::bg(Color::Reset, Fixed::y(clips.read().unwrap().len() as u16, on_bg(border(Map::new(iter, move|clip, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||
let fg = color.lightest.rgb;
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||
Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))),
|
||||
Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))),
|
||||
))))
|
||||
})))))
|
||||
});
|
||||
|
||||
/// 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, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save clip to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PoolCommand {
|
||||
Show(bool),
|
||||
/// Update the contents of the clip pool
|
||||
Clip(MidiPoolCommand),
|
||||
/// Select a clip from the clip pool
|
||||
Select(usize),
|
||||
/// Rename a clip
|
||||
Rename(ClipRenameCommand),
|
||||
/// Change the length of a clip
|
||||
Length(ClipLengthCommand),
|
||||
/// Import from file
|
||||
Import(FileBrowserCommand),
|
||||
/// Export to file
|
||||
Export(FileBrowserCommand),
|
||||
}
|
||||
|
||||
command!(|self:PoolCommand, state: PoolModel|{
|
||||
use PoolCommand::*;
|
||||
match self {
|
||||
Show(visible) => {
|
||||
state.visible = visible;
|
||||
Some(Self::Show(!visible))
|
||||
}
|
||||
Rename(command) => match command {
|
||||
ClipRenameCommand::Begin => {
|
||||
let length = state.clips()[state.clip_index()].read().unwrap().length;
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Rename)
|
||||
},
|
||||
Length(command) => match command {
|
||||
ClipLengthCommand::Begin => {
|
||||
let name = state.clips()[state.clip_index()].read().unwrap().name.clone();
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Rename(state.clip_index(), name)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Length)
|
||||
},
|
||||
Import(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Import(state.clip_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Import)
|
||||
},
|
||||
Export(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Export(state.clip_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(clip) => {
|
||||
state.set_clip_index(clip);
|
||||
None
|
||||
},
|
||||
Clip(command) => command.execute(state)?.map(Clip),
|
||||
}
|
||||
});
|
||||
|
||||
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() {
|
||||
Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::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_clips_command(state, input)?
|
||||
});
|
||||
|
||||
fn to_clips_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PoolCommand as Cmd;
|
||||
let index = state.clip_index();
|
||||
let count = state.clips().len();
|
||||
Some(match input {
|
||||
kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
||||
kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
||||
kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||
kpat!(Char('c')) => Cmd::Clip(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
||||
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||
),
|
||||
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||
index.saturating_add(1) % state.clips().len()
|
||||
),
|
||||
kpat!(Char('<')) => if index > 1 {
|
||||
state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||
Cmd::Clip(MidiPoolCommand::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||
state.set_clip_index(state.clip_index() + 1);
|
||||
Cmd::Clip(MidiPoolCommand::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Delete) => if index > 0 {
|
||||
state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||
Cmd::Clip(MidiPoolCommand::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(MidiPoolCommand::Add(count, MidiClip::new(
|
||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
kpat!(Char('i')) => Cmd::Clip(MidiPoolCommand::Add(index + 1, MidiClip::new(
|
||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
Cmd::Clip(MidiPoolCommand::Add(index + 1, clip))
|
||||
},
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
has_clips!(|self: PoolModel|self.clips);
|
||||
has_clip!(|self: PoolModel|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||
impl PoolModel {
|
||||
pub(crate) fn clip_index (&self) -> usize {
|
||||
self.clip.load(Relaxed)
|
||||
}
|
||||
pub(crate) fn set_clip_index (&self, value: usize) {
|
||||
self.clip.store(value, Relaxed);
|
||||
}
|
||||
pub(crate) fn clips_mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub(crate) fn clips_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
pub fn file_picker (&self) -> Option<&FileBrowser> {
|
||||
match self.mode {
|
||||
Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker),
|
||||
Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
command!(|self: FileBrowserCommand, state: PoolModel|{
|
||||
use PoolMode::*;
|
||||
use FileBrowserCommand::*;
|
||||
let mode = &mut state.mode;
|
||||
match mode {
|
||||
Some(Import(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
Confirm => if browser.is_file() {
|
||||
let index = *index;
|
||||
let path = browser.path();
|
||||
*mode = None;
|
||||
MidiPoolCommand::Import(index, path).execute(state)?;
|
||||
} else if browser.is_dir() {
|
||||
*mode = Some(Import(*index, browser.chdir()?));
|
||||
},
|
||||
_ => todo!(),
|
||||
},
|
||||
Some(Export(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
None
|
||||
});
|
||||
input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{
|
||||
use FileBrowserCommand::*;
|
||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
||||
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
||||
match input {
|
||||
kpat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||
.min(browser.len().saturating_sub(1))),
|
||||
kpat!(Down) => Select(browser.index.saturating_add(1)
|
||||
% browser.len()),
|
||||
kpat!(Right) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Left) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Enter) => Confirm,
|
||||
kpat!(Char(_)) => { todo!() },
|
||||
kpat!(Backspace) => { todo!() },
|
||||
kpat!(Esc) => Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
|
||||
match input {
|
||||
kpat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||
.min(browser.len())),
|
||||
kpat!(Down) => Select(browser.index.saturating_add(1)
|
||||
% browser.len()),
|
||||
kpat!(Right) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Left) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Enter) => Confirm,
|
||||
kpat!(Char(_)) => { todo!() },
|
||||
kpat!(Backspace) => { todo!() },
|
||||
kpat!(Esc) => Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
pub fn new (pulses: usize, focus: Option<ClipLengthFocus>) -> 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) -> Arc<str> {
|
||||
format!("{}", self.bars()).into()
|
||||
}
|
||||
pub fn beats_string (&self) -> Arc<str> {
|
||||
format!("{}", self.beats()).into()
|
||||
}
|
||||
pub fn ticks_string (&self) -> Arc<str> {
|
||||
format!("{:>02}", self.ticks()).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl ClipLengthFocus {
|
||||
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!(TuiOut: (self: ClipLength) => {
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
match self.focus {
|
||||
None =>
|
||||
row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Bar) =>
|
||||
row!("[", bars(), "]", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Beat) =>
|
||||
row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||
Some(ClipLengthFocus::Tick) =>
|
||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum ClipLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
command!(|self: ClipLengthCommand,state:PoolModel|{
|
||||
use ClipLengthCommand::*;
|
||||
use ClipLengthFocus::*;
|
||||
match state.clips_mode_mut().clone() {
|
||||
Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self {
|
||||
Cancel => { *state.clips_mode_mut() = None; },
|
||||
Self::Prev => { focus.prev() },
|
||||
Self::Next => { focus.next() },
|
||||
Self::Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Self::Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Self::Set(length) => {
|
||||
let mut old_length = None;
|
||||
{
|
||||
let mut clip = state.clips()[clip].clone();//.write().unwrap();
|
||||
old_length = Some(clip.read().unwrap().length);
|
||||
clip.write().unwrap().length = length;
|
||||
}
|
||||
*state.clips_mode_mut() = None;
|
||||
return Ok(old_length.map(Self::Set))
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
None
|
||||
});
|
||||
|
||||
input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{
|
||||
if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() {
|
||||
match input {
|
||||
kpat!(Up) => Self::Inc,
|
||||
kpat!(Down) => Self::Dec,
|
||||
kpat!(Right) => Self::Next,
|
||||
kpat!(Left) => Self::Prev,
|
||||
kpat!(Enter) => Self::Set(*length),
|
||||
kpat!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ClipRenameCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(Arc<str>),
|
||||
}
|
||||
|
||||
impl Command<PoolModel> for ClipRenameCommand {
|
||||
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
||||
use ClipRenameCommand::*;
|
||||
match state.clips_mode_mut().clone() {
|
||||
Some(PoolMode::Rename(clip, ref mut old_name)) => match self {
|
||||
Set(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.clips_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Event, PoolModel> for ClipRenameCommand {
|
||||
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.clips_mode() {
|
||||
Some(match input {
|
||||
kpat!(Char(c)) => {
|
||||
let mut new_name = old_name.clone().to_string();
|
||||
new_name.push(*c);
|
||||
Self::Set(new_name.into())
|
||||
},
|
||||
kpat!(Backspace) => {
|
||||
let mut new_name = old_name.clone().to_string();
|
||||
new_name.pop();
|
||||
Self::Set(new_name.into())
|
||||
},
|
||||
kpat!(Enter) => Self::Confirm,
|
||||
kpat!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ pub(crate) use ::tek_tui::{
|
|||
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
||||
pub(crate) use std::fs::File;
|
||||
pub(crate) use std::ops::Deref;
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
|
|
|
|||
|
|
@ -9,13 +9,31 @@ pub struct Sampler {
|
|||
pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
|
||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub midi_in: JackPort<MidiIn>,
|
||||
pub midi_in: Option<JackPort<MidiIn>>,
|
||||
pub audio_ins: Vec<JackPort<AudioIn>>,
|
||||
pub input_meter: Vec<f32>,
|
||||
pub audio_outs: Vec<JackPort<AudioOut>>,
|
||||
pub buffer: Vec<Vec<f32>>,
|
||||
pub output_gain: f32
|
||||
}
|
||||
impl Default for Sampler {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
midi_in: None,
|
||||
audio_ins: vec![],
|
||||
input_meter: vec![0.0;2],
|
||||
audio_outs: vec![],
|
||||
jack: Default::default(),
|
||||
name: "tek_sampler".to_string(),
|
||||
mapped: [const { None };128],
|
||||
unmapped: vec![],
|
||||
voices: Arc::new(RwLock::new(vec![])),
|
||||
buffer: vec![vec![0.0;16384];2],
|
||||
output_gain: 1.,
|
||||
recording: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Sampler {
|
||||
pub fn new (
|
||||
jack: &Arc<RwLock<JackConnection>>,
|
||||
|
|
@ -26,24 +44,16 @@ impl Sampler {
|
|||
) -> Usually<Self> {
|
||||
let name = name.as_ref();
|
||||
Ok(Self {
|
||||
midi_in: JackPort::<MidiIn>::new(jack, format!("M/{name}"), midi_from)?,
|
||||
midi_in: Some(JackPort::<MidiIn>::new(jack, format!("M/{name}"), midi_from)?),
|
||||
audio_ins: vec![
|
||||
JackPort::<AudioIn>::new(jack, &format!("L/{name}"), audio_from[0])?,
|
||||
JackPort::<AudioIn>::new(jack, &format!("R/{name}"), audio_from[1])?,
|
||||
],
|
||||
input_meter: vec![0.0;2],
|
||||
audio_outs: vec![
|
||||
JackPort::<AudioOut>::new(jack, &format!("{name}/L"), audio_to[0])?,
|
||||
JackPort::<AudioOut>::new(jack, &format!("{name}/R"), audio_to[1])?,
|
||||
],
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
mapped: [const { None };128],
|
||||
unmapped: vec![],
|
||||
voices: Arc::new(RwLock::new(vec![])),
|
||||
buffer: vec![vec![0.0;16384];2],
|
||||
output_gain: 1.,
|
||||
recording: None,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
pub fn cancel_recording (&mut self) {
|
||||
|
|
@ -112,18 +122,20 @@ impl Sampler {
|
|||
/// Create [Voice]s from [Sample]s in response to MIDI input.
|
||||
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
|
||||
let Sampler { midi_in, mapped, voices, .. } = self;
|
||||
for RawMidi { time, bytes } in midi_in.port.iter(scope) {
|
||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||
match message {
|
||||
MidiMessage::NoteOn { ref key, ref vel } => {
|
||||
if let Some(ref sample) = mapped[key.as_int() as usize] {
|
||||
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
|
||||
if let Some(ref midi_in) = midi_in {
|
||||
for RawMidi { time, bytes } in midi_in.port.iter(scope) {
|
||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||
match message {
|
||||
MidiMessage::NoteOn { ref key, ref vel } => {
|
||||
if let Some(ref sample) = mapped[key.as_int() as usize] {
|
||||
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
|
||||
}
|
||||
},
|
||||
MidiMessage::Controller { controller, value } => {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
MidiMessage::Controller { controller, value } => {
|
||||
// TODO
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use crate::*;
|
||||
use ClockCommand::{Play, Pause};
|
||||
use self::ArrangerCommand as Cmd;
|
||||
has_clock!(|self: Arranger|&self.clock);
|
||||
has_clips!(|self: Arranger|self.pool.clips);
|
||||
has_editor!(|self: Arranger|self.editor);
|
||||
impl Arranger {
|
||||
pub fn activate (&mut self) -> Usually<()> {
|
||||
if let ArrangerSelection::Scene(s) = self.selected {
|
||||
|
|
@ -41,6 +38,139 @@ impl Arranger {
|
|||
}
|
||||
}
|
||||
}
|
||||
impl Arranger {
|
||||
pub fn track_next_name (&self) -> Arc<str> {
|
||||
format!("Trk{:02}", self.tracks.len() + 1).into()
|
||||
}
|
||||
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
||||
let track = ArrangerTrack {
|
||||
width: (name.len() + 2).max(9),
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
player: MidiPlayer::from(&self.clock),
|
||||
name,
|
||||
};
|
||||
self.tracks.push(track);
|
||||
let len = self.tracks.len();
|
||||
let index = len - 1;
|
||||
for scene in self.scenes.iter_mut() {
|
||||
while scene.clips.len() < len {
|
||||
scene.clips.push(None);
|
||||
}
|
||||
}
|
||||
Ok(&mut self.tracks[index])
|
||||
}
|
||||
pub fn track_del (&mut self, index: usize) {
|
||||
self.tracks.remove(index);
|
||||
for scene in self.scenes.iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
pub fn tracks_add (
|
||||
&mut self,
|
||||
count: usize,
|
||||
width: usize,
|
||||
midi_from: &[PortConnection],
|
||||
midi_to: &[PortConnection],
|
||||
) -> Usually<()> {
|
||||
let jack = self.jack.clone();
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..count {
|
||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||
let mut track = self.track_add(None, Some(color))?;
|
||||
track.width = width;
|
||||
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
||||
track.player.midi_ins.push(port);
|
||||
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
||||
track.player.midi_outs.push(port);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Arranger {
|
||||
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let scene = ArrangerScene {
|
||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||
clips: vec![None;self.tracks.len()],
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
};
|
||||
self.scenes.push(scene);
|
||||
let index = self.scenes.len() - 1;
|
||||
Ok(&mut self.scenes[index])
|
||||
}
|
||||
pub fn scene_del (&mut self, index: usize) {
|
||||
todo!("delete scene");
|
||||
}
|
||||
fn scene_default_name (&self) -> Arc<str> {
|
||||
format!("Sc{:3>}", self.scenes.len() + 1).into()
|
||||
}
|
||||
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().and_then(|s|self.scenes.get(s))
|
||||
}
|
||||
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
|
||||
}
|
||||
pub fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _scene = self.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl ArrangerTrack {
|
||||
fn longest_name (tracks: &[Self]) -> usize {
|
||||
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
fn width_inc (&mut self) {
|
||||
self.width += 1;
|
||||
}
|
||||
fn width_dec (&mut self) {
|
||||
if self.width > Arranger::TRACK_MIN_WIDTH {
|
||||
self.width -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 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 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(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||
*clip.read().unwrap() == *c.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
}
|
||||
|
||||
//pub struct ArrangerVCursor {
|
||||
//cols: Vec<(usize, usize)>,
|
||||
|
|
@ -181,110 +311,6 @@ impl Arranger {
|
|||
//TuiTheme::g(32).into(),
|
||||
//TuiTheme::g(32).into(),
|
||||
//);
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangerSelection {
|
||||
/// The whole mix is selected
|
||||
#[default] 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 is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
||||
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
||||
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
||||
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
||||
pub fn description (
|
||||
&self,
|
||||
tracks: &[ArrangerTrack],
|
||||
scenes: &[ArrangerScene],
|
||||
) -> Arc<str> {
|
||||
format!("Selected: {}", match self {
|
||||
Self::Mix => "Everything".to_string(),
|
||||
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
||||
.unwrap_or_else(||"T??".into()),
|
||||
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
||||
.unwrap_or_else(||"S??".into()),
|
||||
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"),
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
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 Arranger {
|
||||
pub fn track_next_name (&self) -> Arc<str> {
|
||||
format!("Trk{:02}", self.tracks.len() + 1).into()
|
||||
}
|
||||
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
||||
let track = ArrangerTrack {
|
||||
width: (name.len() + 2).max(9),
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
player: MidiPlayer::from(&self.clock),
|
||||
name,
|
||||
};
|
||||
self.tracks.push(track);
|
||||
let len = self.tracks.len();
|
||||
let index = len - 1;
|
||||
for scene in self.scenes.iter_mut() {
|
||||
while scene.clips.len() < len {
|
||||
scene.clips.push(None);
|
||||
}
|
||||
}
|
||||
Ok(&mut self.tracks[index])
|
||||
}
|
||||
pub fn track_del (&mut self, index: usize) {
|
||||
self.tracks.remove(index);
|
||||
for scene in self.scenes.iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
pub fn tracks_add (
|
||||
&mut self,
|
||||
count: usize,
|
||||
width: usize,
|
||||
midi_from: &[PortConnection],
|
||||
midi_to: &[PortConnection],
|
||||
) -> Usually<()> {
|
||||
let jack = self.jack.clone();
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..count {
|
||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||
let mut track = self.track_add(None, Some(color))?;
|
||||
track.width = width;
|
||||
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
||||
track.player.midi_ins.push(port);
|
||||
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
||||
track.player.midi_outs.push(port);
|
||||
}
|
||||
// TODO: port per track:
|
||||
//for connection in midi_from.iter() {
|
||||
//let mut split = connection.as_ref().split("=");
|
||||
|
|
@ -332,107 +358,3 @@ impl Arranger {
|
|||
//panic!("Failed to parse track number: {number}")
|
||||
//}
|
||||
//}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug)] pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub name: Arc<str>,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// Identifying color of track
|
||||
pub color: ItemPalette,
|
||||
/// MIDI player state
|
||||
pub player: MidiPlayer,
|
||||
}
|
||||
has_clock!(|self:ArrangerTrack|self.player.clock());
|
||||
has_player!(|self:ArrangerTrack|self.player);
|
||||
impl ArrangerTrack {
|
||||
fn longest_name (tracks: &[Self]) -> usize {
|
||||
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
fn width_inc (&mut self) {
|
||||
self.width += 1;
|
||||
}
|
||||
fn width_dec (&mut self) {
|
||||
if self.width > Arranger::TRACK_MIN_WIDTH {
|
||||
self.width -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Arranger {
|
||||
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let scene = ArrangerScene {
|
||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||
clips: vec![None;self.tracks.len()],
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
};
|
||||
self.scenes.push(scene);
|
||||
let index = self.scenes.len() - 1;
|
||||
Ok(&mut self.scenes[index])
|
||||
}
|
||||
pub fn scene_del (&mut self, index: usize) {
|
||||
todo!("delete scene");
|
||||
}
|
||||
fn scene_default_name (&self) -> Arc<str> {
|
||||
format!("Sc{:3>}", self.scenes.len() + 1).into()
|
||||
}
|
||||
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().and_then(|s|self.scenes.get(s))
|
||||
}
|
||||
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
|
||||
}
|
||||
pub fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _scene = self.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<str>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemPalette,
|
||||
}
|
||||
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 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 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(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||
*clip.read().unwrap() == *c.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
use self::GrooveboxCommand as Cmd;
|
||||
use EdnItem::*;
|
||||
use std::marker::ConstParamTy;
|
||||
|
|
@ -15,11 +15,8 @@ pub mod view; pub use self::view::*;
|
|||
pub mod control; pub use self::control::*;
|
||||
pub mod audio; pub use self::audio::*;
|
||||
|
||||
pub mod arranger; pub use self::arranger::*;
|
||||
pub mod groovebox; pub use self::groovebox::*;
|
||||
pub mod mixer; pub use self::mixer::*;
|
||||
pub mod pool; pub use self::pool::*;
|
||||
pub mod sequencer; pub use self::sequencer::*;
|
||||
pub mod arranger; pub use self::arranger::*;
|
||||
pub mod mixer; pub use self::mixer::*;
|
||||
|
||||
pub use ::tek_time; pub use ::tek_time::*;
|
||||
pub use ::tek_jack; pub use ::tek_jack::{*, jack::{*, contrib::*}};
|
||||
|
|
|
|||
100
tek/src/model.rs
100
tek/src/model.rs
|
|
@ -1,6 +1,5 @@
|
|||
use crate::*;
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
#[derive(Default)] pub struct App {
|
||||
pub jack: Arc<RwLock<JackConnection>>,
|
||||
pub edn: String,
|
||||
pub clock: Clock,
|
||||
|
|
@ -33,7 +32,7 @@ impl App {
|
|||
midi_tos: &[PortConnection],
|
||||
) -> Self {
|
||||
Self {
|
||||
edn: include_str!("sequencer.edn").to_string(),
|
||||
edn: include_str!("../edn/sequencer.edn").to_string(),
|
||||
jack: jack.clone(),
|
||||
pool: Some(pool),
|
||||
editor: Some(editor),
|
||||
|
|
@ -56,7 +55,7 @@ impl App {
|
|||
audio_tos: &[&[PortConnection]],
|
||||
) -> Self {
|
||||
Self {
|
||||
edn: include_str!("groovebox.edn").to_string(),
|
||||
edn: include_str!("../edn/groovebox.edn").to_string(),
|
||||
sampler: Some(sampler),
|
||||
..Self::sequencer(
|
||||
jack, pool, editor,
|
||||
|
|
@ -78,7 +77,7 @@ impl App {
|
|||
track_width: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
edn: include_str!("arranger.edn").to_string(),
|
||||
edn: include_str!("../edn/arranger.edn").to_string(),
|
||||
..Self::groovebox(
|
||||
jack, pool, editor,
|
||||
None, midi_froms, midi_tos,
|
||||
|
|
@ -87,7 +86,7 @@ impl App {
|
|||
}
|
||||
}
|
||||
}
|
||||
pub struct Sequencer {
|
||||
#[derive(Default)] pub struct Sequencer {
|
||||
pub jack: Arc<RwLock<JackConnection>>,
|
||||
pub compact: bool,
|
||||
pub editor: MidiEditor,
|
||||
|
|
@ -101,7 +100,12 @@ pub struct Sequencer {
|
|||
pub status: bool,
|
||||
pub transport: bool,
|
||||
}
|
||||
pub struct Groovebox {
|
||||
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
||||
has_clock!(|self:Sequencer|&self.player.clock);
|
||||
has_clips!(|self:Sequencer|self.pool.clips);
|
||||
has_editor!(|self:Sequencer|self.editor);
|
||||
|
||||
#[derive(Default)] pub struct Groovebox {
|
||||
pub jack: Arc<RwLock<JackConnection>>,
|
||||
pub compact: bool,
|
||||
pub editor: MidiEditor,
|
||||
|
|
@ -114,7 +118,9 @@ pub struct Groovebox {
|
|||
pub size: Measure<TuiOut>,
|
||||
pub status: bool,
|
||||
}
|
||||
pub struct Arranger {
|
||||
has_clock!(|self: Groovebox|self.player.clock());
|
||||
|
||||
#[derive(Default)] pub struct Arranger {
|
||||
pub clock: Clock,
|
||||
pub color: ItemPalette,
|
||||
pub compact: bool,
|
||||
|
|
@ -133,3 +139,81 @@ pub struct Arranger {
|
|||
pub splits: [u16;2],
|
||||
pub tracks: Vec<ArrangerTrack>,
|
||||
}
|
||||
has_clock!(|self: Arranger|&self.clock);
|
||||
has_clips!(|self: Arranger|self.pool.clips);
|
||||
has_editor!(|self: Arranger|self.editor);
|
||||
#[derive(Debug)] pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub name: Arc<str>,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// Identifying color of track
|
||||
pub color: ItemPalette,
|
||||
/// MIDI player state
|
||||
pub player: MidiPlayer,
|
||||
}
|
||||
has_clock!(|self:ArrangerTrack|self.player.clock());
|
||||
has_player!(|self:ArrangerTrack|self.player);
|
||||
#[derive(Default)] pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<str>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemPalette,
|
||||
}
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangerSelection {
|
||||
/// The whole mix is selected
|
||||
#[default] 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 is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
||||
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
||||
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
||||
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
||||
pub fn description (
|
||||
&self,
|
||||
tracks: &[ArrangerTrack],
|
||||
scenes: &[ArrangerScene],
|
||||
) -> Arc<str> {
|
||||
format!("Selected: {}", match self {
|
||||
Self::Mix => "Everything".to_string(),
|
||||
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
||||
.unwrap_or_else(||"T??".into()),
|
||||
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
||||
.unwrap_or_else(||"S??".into()),
|
||||
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"),
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
495
tek/src/pool.rs
495
tek/src/pool.rs
|
|
@ -1,495 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
use ClipLengthFocus::*;
|
||||
use ClipLengthCommand::*;
|
||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolModel {
|
||||
pub(crate) visible: bool,
|
||||
/// Collection of clips
|
||||
pub(crate) clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Selected clip
|
||||
pub(crate) clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PoolMode>,
|
||||
/// Rendered size
|
||||
size: Measure<TuiOut>,
|
||||
/// Scroll offset
|
||||
scroll: usize,
|
||||
}
|
||||
impl Default for PoolModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
clip: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
size: Measure::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|PoolModel = {
|
||||
let mut model = Self::default();
|
||||
model.clips.write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
|
||||
pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
|
||||
render!(TuiOut: (self: PoolView<'a>) => {
|
||||
let Self(compact, model) = self;
|
||||
let PoolModel { clips, mode, .. } = self.1;
|
||||
let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into());
|
||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||
let iter = | |model.clips().clone().into_iter();
|
||||
Tui::bg(Color::Reset, Fixed::y(clips.read().unwrap().len() as u16, on_bg(border(Map::new(iter, move|clip, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||
let fg = color.lightest.rgb;
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||
Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))),
|
||||
Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))),
|
||||
))))
|
||||
})))))
|
||||
});
|
||||
|
||||
/// 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, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save clip to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PoolCommand {
|
||||
Show(bool),
|
||||
/// Update the contents of the clip pool
|
||||
Clip(MidiPoolCommand),
|
||||
/// Select a clip from the clip pool
|
||||
Select(usize),
|
||||
/// Rename a clip
|
||||
Rename(ClipRenameCommand),
|
||||
/// Change the length of a clip
|
||||
Length(ClipLengthCommand),
|
||||
/// Import from file
|
||||
Import(FileBrowserCommand),
|
||||
/// Export to file
|
||||
Export(FileBrowserCommand),
|
||||
}
|
||||
|
||||
command!(|self:PoolCommand, state: PoolModel|{
|
||||
use PoolCommand::*;
|
||||
match self {
|
||||
Show(visible) => {
|
||||
state.visible = visible;
|
||||
Some(Self::Show(!visible))
|
||||
}
|
||||
Rename(command) => match command {
|
||||
ClipRenameCommand::Begin => {
|
||||
let length = state.clips()[state.clip_index()].read().unwrap().length;
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Rename)
|
||||
},
|
||||
Length(command) => match command {
|
||||
ClipLengthCommand::Begin => {
|
||||
let name = state.clips()[state.clip_index()].read().unwrap().name.clone();
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Rename(state.clip_index(), name)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Length)
|
||||
},
|
||||
Import(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Import(state.clip_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Import)
|
||||
},
|
||||
Export(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.clips_mode_mut() = Some(
|
||||
PoolMode::Export(state.clip_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(clip) => {
|
||||
state.set_clip_index(clip);
|
||||
None
|
||||
},
|
||||
Clip(command) => command.execute(state)?.map(Clip),
|
||||
}
|
||||
});
|
||||
|
||||
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() {
|
||||
Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::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_clips_command(state, input)?
|
||||
});
|
||||
|
||||
fn to_clips_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PoolCommand as Cmd;
|
||||
let index = state.clip_index();
|
||||
let count = state.clips().len();
|
||||
Some(match input {
|
||||
kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
||||
kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
||||
kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||
kpat!(Char('c')) => Cmd::Clip(MidiPoolCommand::SetColor(index, ItemColor::random())),
|
||||
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||
),
|
||||
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||
index.saturating_add(1) % state.clips().len()
|
||||
),
|
||||
kpat!(Char('<')) => if index > 1 {
|
||||
state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||
Cmd::Clip(MidiPoolCommand::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||
state.set_clip_index(state.clip_index() + 1);
|
||||
Cmd::Clip(MidiPoolCommand::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Delete) => if index > 0 {
|
||||
state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||
Cmd::Clip(MidiPoolCommand::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(MidiPoolCommand::Add(count, MidiClip::new(
|
||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
kpat!(Char('i')) => Cmd::Clip(MidiPoolCommand::Add(index + 1, MidiClip::new(
|
||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
Cmd::Clip(MidiPoolCommand::Add(index + 1, clip))
|
||||
},
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
has_clips!(|self: PoolModel|self.clips);
|
||||
has_clip!(|self: PoolModel|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||
impl PoolModel {
|
||||
pub(crate) fn clip_index (&self) -> usize {
|
||||
self.clip.load(Relaxed)
|
||||
}
|
||||
pub(crate) fn set_clip_index (&self, value: usize) {
|
||||
self.clip.store(value, Relaxed);
|
||||
}
|
||||
pub(crate) fn clips_mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub(crate) fn clips_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
pub fn file_picker (&self) -> Option<&FileBrowser> {
|
||||
match self.mode {
|
||||
Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker),
|
||||
Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
command!(|self: FileBrowserCommand, state: PoolModel|{
|
||||
use PoolMode::*;
|
||||
use FileBrowserCommand::*;
|
||||
let mode = &mut state.mode;
|
||||
match mode {
|
||||
Some(Import(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
Confirm => if browser.is_file() {
|
||||
let index = *index;
|
||||
let path = browser.path();
|
||||
*mode = None;
|
||||
MidiPoolCommand::Import(index, path).execute(state)?;
|
||||
} else if browser.is_dir() {
|
||||
*mode = Some(Import(*index, browser.chdir()?));
|
||||
},
|
||||
_ => todo!(),
|
||||
},
|
||||
Some(Export(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
None
|
||||
});
|
||||
input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{
|
||||
use FileBrowserCommand::*;
|
||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
||||
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
||||
match input {
|
||||
kpat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||
.min(browser.len().saturating_sub(1))),
|
||||
kpat!(Down) => Select(browser.index.saturating_add(1)
|
||||
% browser.len()),
|
||||
kpat!(Right) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Left) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Enter) => Confirm,
|
||||
kpat!(Char(_)) => { todo!() },
|
||||
kpat!(Backspace) => { todo!() },
|
||||
kpat!(Esc) => Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
|
||||
match input {
|
||||
kpat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||
.min(browser.len())),
|
||||
kpat!(Down) => Select(browser.index.saturating_add(1)
|
||||
% browser.len()),
|
||||
kpat!(Right) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Left) => Chdir(browser.cwd.clone()),
|
||||
kpat!(Enter) => Confirm,
|
||||
kpat!(Char(_)) => { todo!() },
|
||||
kpat!(Backspace) => { todo!() },
|
||||
kpat!(Esc) => Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
pub fn new (pulses: usize, focus: Option<ClipLengthFocus>) -> 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) -> Arc<str> {
|
||||
format!("{}", self.bars()).into()
|
||||
}
|
||||
pub fn beats_string (&self) -> Arc<str> {
|
||||
format!("{}", self.beats()).into()
|
||||
}
|
||||
pub fn ticks_string (&self) -> Arc<str> {
|
||||
format!("{:>02}", self.ticks()).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl ClipLengthFocus {
|
||||
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!(TuiOut: (self: ClipLength) => {
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
match self.focus {
|
||||
None =>
|
||||
row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Bar) =>
|
||||
row!("[", bars(), "]", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Beat) =>
|
||||
row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||
Some(ClipLengthFocus::Tick) =>
|
||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum ClipLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
command!(|self:ClipLengthCommand,state:PoolModel|{
|
||||
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 {
|
||||
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 old_length = None;
|
||||
{
|
||||
let mut clip = state.clips()[clip].clone();//.write().unwrap();
|
||||
old_length = Some(clip.read().unwrap().length);
|
||||
clip.write().unwrap().length = length;
|
||||
}
|
||||
*state.clips_mode_mut() = None;
|
||||
return Ok(old_length.map(Self::Set))
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
None
|
||||
});
|
||||
|
||||
input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{
|
||||
if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() {
|
||||
match input {
|
||||
kpat!(Up) => Self::Inc,
|
||||
kpat!(Down) => Self::Dec,
|
||||
kpat!(Right) => Self::Next,
|
||||
kpat!(Left) => Self::Prev,
|
||||
kpat!(Enter) => Self::Set(*length),
|
||||
kpat!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ClipRenameCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(Arc<str>),
|
||||
}
|
||||
|
||||
impl Command<PoolModel> for ClipRenameCommand {
|
||||
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
||||
use ClipRenameCommand::*;
|
||||
match state.clips_mode_mut().clone() {
|
||||
Some(PoolMode::Rename(clip, ref mut old_name)) => match self {
|
||||
Set(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.clips_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Event, PoolModel> for ClipRenameCommand {
|
||||
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.clips_mode() {
|
||||
Some(match input {
|
||||
kpat!(Char(c)) => {
|
||||
let mut new_name = old_name.clone().to_string();
|
||||
new_name.push(*c);
|
||||
Self::Set(new_name.into())
|
||||
},
|
||||
kpat!(Backspace) => {
|
||||
let mut new_name = old_name.clone().to_string();
|
||||
new_name.pop();
|
||||
Self::Set(new_name.into())
|
||||
},
|
||||
kpat!(Enter) => Self::Confirm,
|
||||
kpat!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
use crate::*;
|
||||
use ClockCommand::{Play, Pause};
|
||||
use KeyCode::{Tab, Char};
|
||||
use SequencerCommand as Cmd;
|
||||
use MidiEditCommand::*;
|
||||
use MidiPoolCommand::*;
|
||||
has_size!(<TuiOut>|self:Sequencer|&self.size);
|
||||
has_clock!(|self:Sequencer|&self.player.clock);
|
||||
has_clips!(|self:Sequencer|self.pool.clips);
|
||||
has_editor!(|self:Sequencer|self.editor);
|
||||
|
|
@ -354,7 +354,7 @@ impl EdnViewData<TuiOut> for &Sequencer {
|
|||
}
|
||||
}
|
||||
impl Sequencer {
|
||||
const EDN: &'static str = include_str!("sequencer.edn");
|
||||
const EDN: &'static str = include_str!("../edn/sequencer.edn");
|
||||
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
|
||||
}
|
||||
|
|
@ -429,7 +429,7 @@ impl EdnViewData<TuiOut> for &Groovebox {
|
|||
}
|
||||
}
|
||||
impl Groovebox {
|
||||
const EDN: &'static str = include_str!("groovebox.edn");
|
||||
const EDN: &'static str = include_str!("../edn/groovebox.edn");
|
||||
fn toolbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::x(Fixed::y(2, lay!(
|
||||
Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))),
|
||||
|
|
@ -466,7 +466,6 @@ impl Groovebox {
|
|||
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor))))
|
||||
}
|
||||
}
|
||||
has_clock!(|self: Groovebox|self.player.clock());
|
||||
|
||||
///// Status bar for sequencer app
|
||||
//#[derive(Clone)]
|
||||
|
|
@ -600,7 +599,7 @@ impl EdnViewData<TuiOut> for &Arranger {
|
|||
}
|
||||
}
|
||||
impl Arranger {
|
||||
const EDN: &'static str = include_str!("arranger.edn");
|
||||
const EDN: &'static str = include_str!("../edn/arranger.edn");
|
||||
pub const LEFT_SEP: char = '▎';
|
||||
pub const TRACK_MIN_WIDTH: usize = 9;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue