mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
midi import/export browser, pt.1
This commit is contained in:
parent
26eb5f0315
commit
6d8dbc6780
5 changed files with 263 additions and 140 deletions
|
|
@ -903,6 +903,16 @@ impl<E: Engine, A: Widget<Engine = E>, B: Widget<Engine = E>> Widget for Split<E
|
||||||
/// A widget that tracks its render width and height
|
/// A widget that tracks its render width and height
|
||||||
pub struct Measure<E: Engine>(PhantomData<E>, AtomicUsize, AtomicUsize);
|
pub struct Measure<E: Engine>(PhantomData<E>, AtomicUsize, AtomicUsize);
|
||||||
|
|
||||||
|
impl<E: Engine> Clone for Measure<E> {
|
||||||
|
fn clone (&self) -> Self {
|
||||||
|
Self(
|
||||||
|
Default::default(),
|
||||||
|
AtomicUsize::from(self.1.load(Ordering::Relaxed)),
|
||||||
|
AtomicUsize::from(self.2.load(Ordering::Relaxed)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<E: Engine> std::fmt::Debug for Measure<E> {
|
impl<E: Engine> std::fmt::Debug for Measure<E> {
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
f.debug_struct("Measure")
|
f.debug_struct("Measure")
|
||||||
|
|
|
||||||
|
|
@ -654,3 +654,37 @@ pub const fn shift (key: KeyEvent) -> KeyEvent {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[macro_export] macro_rules! key_lit {
|
||||||
|
($code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Ctrl-$code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Alt-$code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::ALT,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
(Shift-$code:expr) => {
|
||||||
|
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||||
|
code: $code,
|
||||||
|
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
||||||
|
kind: crossterm::event::KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ pub enum PhrasesCommand {
|
||||||
Phrase(PhrasePoolCommand),
|
Phrase(PhrasePoolCommand),
|
||||||
Rename(PhraseRenameCommand),
|
Rename(PhraseRenameCommand),
|
||||||
Length(PhraseLengthCommand),
|
Length(PhraseLengthCommand),
|
||||||
|
Import(FileBrowserCommand),
|
||||||
|
Export(FileBrowserCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
||||||
|
|
@ -124,8 +126,44 @@ impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
||||||
use PhrasesCommand::*;
|
use PhrasesCommand::*;
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||||
Rename(command) => command.execute(state)?.map(Rename),
|
Rename(command) => match command {
|
||||||
Length(command) => command.execute(state)?.map(Length),
|
PhraseRenameCommand::Begin => {
|
||||||
|
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
||||||
|
*state.phrases_mode_mut() = Some(
|
||||||
|
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => command.execute(state)?.map(Rename)
|
||||||
|
},
|
||||||
|
Length(command) => match command {
|
||||||
|
PhraseLengthCommand::Begin => {
|
||||||
|
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
||||||
|
*state.phrases_mode_mut() = Some(
|
||||||
|
PhrasesMode::Rename(state.phrase_index(), name)
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => command.execute(state)?.map(Length)
|
||||||
|
},
|
||||||
|
Import(command) => match command {
|
||||||
|
FileBrowserCommand::Begin => {
|
||||||
|
*state.phrases_mode_mut() = Some(
|
||||||
|
PhrasesMode::Import(state.phrase_index(), FileBrowser::new())
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => command.execute(state)?.map(Import)
|
||||||
|
},
|
||||||
|
Export(command) => match command {
|
||||||
|
FileBrowserCommand::Begin => {
|
||||||
|
*state.phrases_mode_mut() = Some(
|
||||||
|
PhrasesMode::Export(state.phrase_index(), FileBrowser::new())
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => command.execute(state)?.map(Export)
|
||||||
|
},
|
||||||
Select(phrase) => {
|
Select(phrase) => {
|
||||||
state.set_phrase_index(phrase);
|
state.set_phrase_index(phrase);
|
||||||
None
|
None
|
||||||
|
|
@ -137,21 +175,20 @@ impl<T: PhrasesControl> Command<T> for PhrasesCommand {
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum PhraseLengthCommand {
|
pub enum PhraseLengthCommand {
|
||||||
Begin,
|
Begin,
|
||||||
|
Cancel,
|
||||||
|
Set(usize),
|
||||||
Next,
|
Next,
|
||||||
Prev,
|
Prev,
|
||||||
Inc,
|
Inc,
|
||||||
Dec,
|
Dec,
|
||||||
Set(usize),
|
|
||||||
Cancel,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
||||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||||
use PhraseLengthFocus::*;
|
use PhraseLengthFocus::*;
|
||||||
use PhraseLengthCommand::*;
|
use PhraseLengthCommand::*;
|
||||||
let mut mode = state.phrases_mode_mut().clone();
|
match state.phrases_mode_mut().clone() {
|
||||||
if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = mode {
|
Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self {
|
||||||
match self {
|
|
||||||
Cancel => { *state.phrases_mode_mut() = None; },
|
Cancel => { *state.phrases_mode_mut() = None; },
|
||||||
Prev => { focus.prev() },
|
Prev => { focus.prev() },
|
||||||
Next => { focus.next() },
|
Next => { focus.next() },
|
||||||
|
|
@ -174,15 +211,9 @@ impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
||||||
return Ok(Some(Self::Set(old_length)))
|
return Ok(Some(Self::Set(old_length)))
|
||||||
},
|
},
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
}
|
},
|
||||||
} else if self == Begin {
|
_ => unreachable!()
|
||||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
};
|
||||||
*state.phrases_mode_mut() = Some(
|
|
||||||
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,20 +221,16 @@ impl<T: PhrasesControl> Command<T> for PhraseLengthCommand {
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum PhraseRenameCommand {
|
pub enum PhraseRenameCommand {
|
||||||
Begin,
|
Begin,
|
||||||
Set(String),
|
|
||||||
Confirm,
|
|
||||||
Cancel,
|
Cancel,
|
||||||
|
Confirm,
|
||||||
|
Set(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Command<T> for PhraseRenameCommand
|
impl<T: PhrasesControl> Command<T> for PhraseRenameCommand {
|
||||||
where
|
|
||||||
T: PhrasesControl
|
|
||||||
{
|
|
||||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||||
use PhraseRenameCommand::*;
|
use PhraseRenameCommand::*;
|
||||||
let mut mode = state.phrases_mode_mut().clone();
|
match state.phrases_mode_mut().clone() {
|
||||||
if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = mode {
|
Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self {
|
||||||
match self {
|
|
||||||
Set(s) => {
|
Set(s) => {
|
||||||
state.phrases()[phrase].write().unwrap().name = s.into();
|
state.phrases()[phrase].write().unwrap().name = s.into();
|
||||||
return Ok(Some(Self::Set(old_name.clone())))
|
return Ok(Some(Self::Set(old_name.clone())))
|
||||||
|
|
@ -217,15 +244,40 @@ where
|
||||||
state.phrases()[phrase].write().unwrap().name = old_name.clone();
|
state.phrases()[phrase].write().unwrap().name = old_name.clone();
|
||||||
},
|
},
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
};
|
},
|
||||||
} else if self == Begin {
|
_ => unreachable!()
|
||||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
};
|
||||||
*state.phrases_mode_mut() = Some(
|
Ok(None)
|
||||||
PhrasesMode::Rename(state.phrase_index(), name)
|
}
|
||||||
);
|
}
|
||||||
} else {
|
|
||||||
unreachable!()
|
/// Commands supported by [FileBrowser]
|
||||||
}
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum FileBrowserCommand {
|
||||||
|
Begin,
|
||||||
|
Cancel,
|
||||||
|
Confirm,
|
||||||
|
Select(usize),
|
||||||
|
Chdir(PathBuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: PhrasesControl> Command<T> for FileBrowserCommand {
|
||||||
|
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||||
|
use FileBrowserCommand::*;
|
||||||
|
match state.phrases_mode_mut().clone() {
|
||||||
|
Some(PhrasesMode::Import(index, browser)) => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
Some(PhrasesMode::Export(index, browser)) => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
_ => match self {
|
||||||
|
Begin => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,73 +247,79 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<Arr
|
||||||
|
|
||||||
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhrasesCommand {
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhrasesCommand {
|
||||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||||
use PhrasePoolCommand as Pool;
|
|
||||||
use PhraseRenameCommand as Rename;
|
use PhraseRenameCommand as Rename;
|
||||||
use PhraseLengthCommand as Length;
|
use PhraseLengthCommand as Length;
|
||||||
use KeyCode::{Up, Down, Delete, Char};
|
use FileBrowserCommand as Browse;
|
||||||
let index = state.phrase_index();
|
Some(match state.phrases_mode() {
|
||||||
let count = state.phrases().len();
|
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
|
||||||
Some(match input.event() {
|
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
|
||||||
key!(Up) => Self::Select(
|
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
|
||||||
state.phrase_index().overflowing_sub(1).0.min(state.phrases().len() - 1)
|
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
|
||||||
),
|
_ => to_phrases_command(state, input)?
|
||||||
key!(Down) => Self::Select(
|
|
||||||
state.phrase_index().saturating_add(1) % state.phrases().len()
|
|
||||||
),
|
|
||||||
key!(Char(',')) => if index > 1 {
|
|
||||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
|
||||||
Self::Phrase(Pool::Swap(index - 1, index))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
key!(Char('.')) => if index < count.saturating_sub(1) {
|
|
||||||
state.set_phrase_index(state.phrase_index() + 1);
|
|
||||||
Self::Phrase(Pool::Swap(index + 1, index))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
key!(Delete) => if index > 0 {
|
|
||||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
|
||||||
Self::Phrase(Pool::Delete(index))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
key!(Char('a')) => Self::Phrase(Pool::Add(
|
|
||||||
count,
|
|
||||||
Phrase::new(
|
|
||||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
|
||||||
)
|
|
||||||
)),
|
|
||||||
key!(Char('i')) => Self::Phrase(Pool::Add(
|
|
||||||
index + 1,
|
|
||||||
Phrase::new(
|
|
||||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
|
||||||
)
|
|
||||||
)),
|
|
||||||
key!(Char('d')) => {
|
|
||||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
|
||||||
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
|
||||||
Self::Phrase(Pool::Add(index + 1, phrase))
|
|
||||||
},
|
|
||||||
key!(Char('c')) => Self::Phrase(Pool::SetColor(
|
|
||||||
index,
|
|
||||||
ItemColor::random()
|
|
||||||
)),
|
|
||||||
key!(Char('n')) => Self::Rename(Rename::Begin),
|
|
||||||
key!(Char('t')) => Self::Length(Length::Begin),
|
|
||||||
_ => match state.phrases_mode() {
|
|
||||||
Some(PhrasesMode::Rename(..)) => {
|
|
||||||
Self::Rename(Rename::input_to_command(state, input)?)
|
|
||||||
},
|
|
||||||
Some(PhrasesMode::Length(..)) => {
|
|
||||||
Self::Length(Length::input_to_command(state, input)?)
|
|
||||||
},
|
|
||||||
_ => return None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_phrases_command <T: PhrasesControl> (state: &T, input: &TuiInput) -> Option<PhrasesCommand> {
|
||||||
|
use KeyCode::{Up, Down, Delete, Char};
|
||||||
|
use PhrasesCommand as Cmd;
|
||||||
|
use PhrasePoolCommand as Pool;
|
||||||
|
use PhraseRenameCommand as Rename;
|
||||||
|
use PhraseLengthCommand as Length;
|
||||||
|
use FileBrowserCommand as Browse;
|
||||||
|
let index = state.phrase_index();
|
||||||
|
let count = state.phrases().len();
|
||||||
|
Some(match input.event() {
|
||||||
|
key!(Char('n')) => Cmd::Rename(Rename::Begin),
|
||||||
|
key!(Char('t')) => Cmd::Length(Length::Begin),
|
||||||
|
key!(Char('m')) => Cmd::Import(Browse::Begin),
|
||||||
|
key!(Char('x')) => Cmd::Export(Browse::Begin),
|
||||||
|
key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)),
|
||||||
|
key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()),
|
||||||
|
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
||||||
|
key!(Char(',')) => if index > 1 {
|
||||||
|
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
||||||
|
Cmd::Phrase(Pool::Swap(index - 1, index))
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
},
|
||||||
|
key!(Char('.')) => if index < count.saturating_sub(1) {
|
||||||
|
state.set_phrase_index(state.phrase_index() + 1);
|
||||||
|
Cmd::Phrase(Pool::Swap(index + 1, index))
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
},
|
||||||
|
key!(Delete) => if index > 0 {
|
||||||
|
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
||||||
|
Cmd::Phrase(Pool::Delete(index))
|
||||||
|
} else {
|
||||||
|
return None
|
||||||
|
},
|
||||||
|
key!(Char('a')) => Cmd::Phrase(Pool::Add(
|
||||||
|
count, Phrase::new(
|
||||||
|
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
key!(Char('i')) => Cmd::Phrase(Pool::Add(
|
||||||
|
index + 1, Phrase::new(
|
||||||
|
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
key!(Char('d')) => {
|
||||||
|
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||||
|
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
||||||
|
Cmd::Phrase(Pool::Add(index + 1, phrase))
|
||||||
|
},
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for FileBrowserCommand {
|
||||||
|
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseLengthCommand {
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseLengthCommand {
|
||||||
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
||||||
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
|
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,72 @@ impl Default for PhrasesModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modes for phrase pool
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PhrasesMode {
|
||||||
|
/// Renaming a pattern
|
||||||
|
Rename(usize, String),
|
||||||
|
/// Editing the length of a pattern
|
||||||
|
Length(usize, usize, PhraseLengthFocus),
|
||||||
|
/// Load phrase from disk
|
||||||
|
Import(usize, FileBrowser),
|
||||||
|
/// Save phrase to disk
|
||||||
|
Export(usize, FileBrowser),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browses for phrase to import/export
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileBrowser {
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
pub dirs: Vec<PathBuf>,
|
||||||
|
pub files: Vec<PathBuf>,
|
||||||
|
pub index: usize,
|
||||||
|
pub scroll: usize,
|
||||||
|
pub size: Measure<Tui>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileBrowser {
|
||||||
|
pub fn new () -> Self {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays and edits phrase length.
|
||||||
|
pub struct PhraseLength {
|
||||||
|
/// Pulses per beat (quaver)
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Beats per bar
|
||||||
|
pub bpb: usize,
|
||||||
|
/// Length of phrase in pulses
|
||||||
|
pub pulses: usize,
|
||||||
|
/// Selected subdivision
|
||||||
|
pub focus: Option<PhraseLengthFocus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhraseLength {
|
||||||
|
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||||
|
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||||
|
}
|
||||||
|
pub fn bars (&self) -> usize {
|
||||||
|
self.pulses / (self.bpb * self.ppq)
|
||||||
|
}
|
||||||
|
pub fn beats (&self) -> usize {
|
||||||
|
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||||
|
}
|
||||||
|
pub fn ticks (&self) -> usize {
|
||||||
|
self.pulses % self.ppq
|
||||||
|
}
|
||||||
|
pub fn bars_string (&self) -> String {
|
||||||
|
format!("{}", self.bars())
|
||||||
|
}
|
||||||
|
pub fn beats_string (&self) -> String {
|
||||||
|
format!("{}", self.beats())
|
||||||
|
}
|
||||||
|
pub fn ticks_string (&self) -> String {
|
||||||
|
format!("{:>02}", self.ticks())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl HasScenes<ArrangerScene> for ArrangerTui {
|
impl HasScenes<ArrangerScene> for ArrangerTui {
|
||||||
fn scenes (&self) -> &Vec<ArrangerScene> {
|
fn scenes (&self) -> &Vec<ArrangerScene> {
|
||||||
&self.scenes
|
&self.scenes
|
||||||
|
|
@ -267,48 +333,3 @@ impl ArrangerTrackApi for ArrangerTrack {
|
||||||
self.color
|
self.color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modes for phrase pool
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PhrasesMode {
|
|
||||||
/// Renaming a pattern
|
|
||||||
Rename(usize, String),
|
|
||||||
/// Editing the length of a pattern
|
|
||||||
Length(usize, usize, PhraseLengthFocus),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays and edits phrase length.
|
|
||||||
pub struct PhraseLength {
|
|
||||||
/// Pulses per beat (quaver)
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Beats per bar
|
|
||||||
pub bpb: usize,
|
|
||||||
/// Length of phrase in pulses
|
|
||||||
pub pulses: usize,
|
|
||||||
/// Selected subdivision
|
|
||||||
pub focus: Option<PhraseLengthFocus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PhraseLength {
|
|
||||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
|
||||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
|
||||||
}
|
|
||||||
pub fn bars (&self) -> usize {
|
|
||||||
self.pulses / (self.bpb * self.ppq)
|
|
||||||
}
|
|
||||||
pub fn beats (&self) -> usize {
|
|
||||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
|
||||||
}
|
|
||||||
pub fn ticks (&self) -> usize {
|
|
||||||
self.pulses % self.ppq
|
|
||||||
}
|
|
||||||
pub fn bars_string (&self) -> String {
|
|
||||||
format!("{}", self.bars())
|
|
||||||
}
|
|
||||||
pub fn beats_string (&self) -> String {
|
|
||||||
format!("{}", self.beats())
|
|
||||||
}
|
|
||||||
pub fn ticks_string (&self) -> String {
|
|
||||||
format!("{:>02}", self.ticks())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue