wip: refactor pt.4, reduce number of files

This commit is contained in:
🪞👃🪞 2024-11-10 01:03:47 +01:00
parent adf5b3f0f8
commit 8c37c95cc6
60 changed files with 2185 additions and 2187 deletions

View file

@ -1,101 +0,0 @@
use crate::*;
#[derive(Clone)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
Arrangement(ArrangementCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
#[derive(Clone)]
pub enum ArrangementCommand {
New,
Load,
Save,
ToggleViewMode,
Delete,
Activate,
Increment,
Decrement,
ZoomIn,
ZoomOut,
MoveBack,
MoveForward,
RandomColor,
Put,
Get,
AddScene,
AddTrack,
ToggleLoop,
GoUp,
GoDown,
GoLeft,
GoRight,
Edit(Option<Arc<RwLock<Phrase>>>),
}
impl<E: Engine> Command<Arranger<E>> for ArrangerCommand {
fn execute (self, state: &mut Arranger<E>) -> Perhaps<Self> {
let undo = match self {
Self::Focus(cmd) => {
delegate(cmd, Self::Focus, state)
},
Self::Phrases(cmd) => {
delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
},
Self::Editor(cmd) => {
delegate(cmd, Self::Editor, &mut state.editor)
},
Self::Arrangement(cmd) => {
delegate(cmd, Self::Arrangement, &mut state.arrangement)
},
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
} else {
Ok(None)
},
Self::EditPhrase(phrase) => {
state.editor.phrase = phrase.clone();
state.focus(ArrangerFocus::PhraseEditor);
state.focus_enter();
Ok(None)
}
}?;
state.show_phrase();
state.update_status();
return Ok(undo);
}
}
impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
fn execute (self, state: &mut Arrangement<E>) -> Perhaps<Self> {
use ArrangementCommand::*;
match self {
New => todo!(),
Load => todo!(),
Save => todo!(),
Edit(phrase) => { state.phrase = phrase.clone() },
ToggleViewMode => { state.mode.to_next(); },
Delete => { state.delete(); },
Activate => { state.activate(); },
Increment => { state.increment(); },
Decrement => { state.decrement(); },
ZoomIn => { state.zoom_in(); },
ZoomOut => { state.zoom_out(); },
MoveBack => { state.move_back(); },
MoveForward => { state.move_forward(); },
RandomColor => { state.randomize_color(); },
Put => { state.phrase_put(); },
Get => { state.phrase_get(); },
AddScene => { state.scene_add(None, None)?; },
AddTrack => { state.track_add(None, None)?; },
ToggleLoop => { state.toggle_loop() },
GoUp => { state.go_up() },
GoDown => { state.go_down() },
GoLeft => { state.go_left() },
GoRight => { state.go_right() },
};
Ok(None)
}
}

View file

@ -1,54 +0,0 @@
use crate::*;
/// Layout for standalone arranger app.
impl Content for Arranger<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.arrangement.focused;
let border_bg = Arranger::<Tui>::border_bg();
let border_fg = Arranger::<Tui>::border_fg(focused);
let title_fg = Arranger::<Tui>::title_fg(focused);
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
let entered = if self.arrangement.entered { "" } else { " " };
Split::down(
1,
row!(menu in self.menu.menus.iter() => {
row!(" ", menu.title.as_str(), " ")
}),
Split::up(
1,
widget(&self.status),
Split::up(
1,
widget(&self.transport),
Split::down(
self.arrangement_split,
lay!(
widget(&self.arrangement).grow_y(1).border(border),
widget(&self.arrangement.size),
widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1),
),
Split::right(
self.phrases_split,
self.phrases.clone(),
widget(&self.editor),
)
)
)
)
)
}
}
impl Content for Arrangement<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move |add|{
match self.mode {
ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) },
ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) },
}?;
add(&self.size)
})
}
}

View file

@ -10,36 +10,24 @@ pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
submod! {
arranger
arranger_cmd
arranger_snd
arranger_tui
arranger_tui_bar
arranger_tui_cmd
arranger_tui_col
arranger_tui_hor
arranger_tui_ver
sequencer
sequencer_cmd
sequencer_snd
sequencer_tui
transport
transport_cmd
transport_snd
transport_tui
mixer
mixer_snd
mixer_cmd
mixer_tui
sampler
sampler_snd
sampler_cmd
plugin
plugin_snd
plugin_cmd
plugin_tui
plugin_lv2
plugin_lv2_gui
plugin_vst2
plugin_vst3
tui_arranger
tui_arranger_bar
tui_arranger_cmd
tui_arranger_col
tui_arranger_hor
tui_arranger_ver
tui_sequencer
tui_sequencer_cmd
tui_transport
tui_transport_cmd
tui_mixer
tui_mixer_cmd
tui_sampler
tui_sampler_cmd
tui_plugin
tui_plugin_cmd
tui_plugin_lv2
tui_plugin_lv2_gui
tui_plugin_vst2
tui_plugin_vst3
}

View file

@ -1,38 +0,0 @@
use crate::*;
impl Content for Mixer<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::right(|add| {
for channel in self.tracks.iter() {
add(channel)?;
}
Ok(())
})
}
}
impl Content for Track<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
TrackView {
chain: Some(&self),
direction: tek_core::Direction::Right,
focused: true,
entered: true,
//pub channels: u8,
//pub input_ports: Vec<Port<AudioIn>>,
//pub pre_gain_meter: f64,
//pub gain: f64,
//pub insert_ports: Vec<Port<AudioOut>>,
//pub return_ports: Vec<Port<AudioIn>>,
//pub post_gain_meter: f64,
//pub post_insert_meter: f64,
//pub level: f64,
//pub pan: f64,
//pub output_ports: Vec<Port<AudioOut>>,
//pub post_fader_meter: f64,
//pub route: String,
}
}
}

View file

@ -1,33 +0,0 @@
use crate::*;
/// A plugin device.
pub struct Plugin<E> {
_engine: PhantomData<E>,
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub path: Option<String>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub ports: JackPorts,
}
impl<E> Plugin<E> {
/// Create a plugin host device.
pub fn new (
jack: &Arc<RwLock<JackClient>>,
name: &str,
) -> Usually<Self> {
Ok(Self {
_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
ports: JackPorts::default()
})
}
}

View file

@ -1,98 +0,0 @@
use crate::*;
impl Widget for Sampler<Tui> {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
tui_render_sampler(self, to)
}
}
pub fn tui_render_sampler (sampler: &Sampler<Tui>, to: &mut TuiOutput) -> Usually<()> {
let [x, y, _, height] = to.area();
let style = Style::default().gray();
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
let mut width = title.len() + 2;
let mut y1 = 1;
let mut j = 0;
for (note, sample) in sampler.mapped.iter()
.map(|(note, sample)|(Some(note), sample))
.chain(sampler.unmapped.iter().map(|sample|(None, sample)))
{
if y1 >= height {
break
}
let active = j == sampler.cursor.0;
width = width.max(
draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
);
y1 = y1 + 1;
j = j + 1;
}
let height = ((2 + y1) as u16).min(height);
//Ok(Some([x, y, (width as u16).min(to.area().w()), height]))
Ok(())
}
fn draw_sample (
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
impl Widget for AddSampleModal {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
//Align::Center(()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
todo!()
//let area = to.area();
//to.make_dim();
//let area = center_box(
//area,
//64.max(area.w().saturating_sub(8)),
//20.max(area.w().saturating_sub(8)),
//);
//to.fill_fg(area, Color::Reset);
//to.fill_bg(area, Nord::bg_lo(true, true));
//to.fill_char(area, ' ');
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
//for (i, (is_dir, name)) in self.subdirs.iter()
//.map(|path|(true, path))
//.chain(self.files.iter().map(|path|(false, path)))
//.enumerate()
//.skip(self.offset)
//{
//if i >= area.h() as usize - 4 {
//break
//}
//let t = if is_dir { "" } else { "" };
//let line = format!("{t} {}", name.to_string_lossy());
//let line = &line[..line.len().min(area.w() as usize - 4)];
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
//Style::default().green()
//} else {
//Style::default().white()
//}))?;
//}
//Lozenge(Style::default()).draw(to)
}
}

View file

@ -1,481 +0,0 @@
use crate::*;
use std::cmp::PartialEq;
/// MIDI message structural
pub type PhraseData = Vec<Vec<MidiMessage>>;
/// MIDI message serialized
pub type PhraseMessage = Vec<u8>;
/// Collection of serialized MIDI messages
pub type PhraseChunk = [Vec<PhraseMessage>];
/// Root level object for standalone `tek_sequencer`
pub struct Sequencer<E: Engine> {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
/// Controls the JACK transport.
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
/// Global timebase
pub clock: Arc<TransportTime>,
/// Pool of all phrases available to the sequencer
pub phrases: Arc<RwLock<PhrasePool<E>>>,
/// Phrase editor view
pub editor: PhraseEditor<E>,
/// Phrase player
pub player: PhrasePlayer,
/// Which view is focused
pub focus_cursor: (usize, usize),
/// Whether the currently focused item is entered
pub entered: bool,
}
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
/// Status bar for sequencer app
pub enum SequencerStatusBar {
Transport,
PhrasePool,
PhraseEditor,
}
/// Contains all phrases in a project
pub struct PhrasePool<E: Engine> {
_engine: PhantomData<E>,
/// Scroll offset
pub scroll: usize,
/// Highlighted phrase
pub phrase: usize,
/// Phrases in the pool
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Mode switch
pub mode: Option<PhrasePoolMode>,
/// Whether this widget is focused
pub focused: bool,
/// Whether this widget is entered
pub entered: bool,
}
/// Modes for phrase pool
pub enum PhrasePoolMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
}
/// A MIDI sequence.
#[derive(Debug, Clone)] pub struct Phrase {
pub uuid: uuid::Uuid,
/// Name of phrase
pub name: String,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of phrase in pulses
pub length: usize,
/// Notes in phrase
pub notes: PhraseData,
/// Whether to loop the phrase or play it once
pub loop_on: bool,
/// Start of loop
pub loop_start: usize,
/// Length of loop
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of phrase
pub color: ItemColorTriplet,
}
/// Contains state for viewing and editing a phrase
pub struct PhraseEditor<E: Engine> {
_engine: PhantomData<E>,
/// Phrase being played
pub phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub note_len: usize,
/// The full piano keys are rendered to this buffer
pub keys: Buffer,
/// The full piano roll is rendered to this buffer
pub buffer: BigBuffer,
/// Cursor/scroll/zoom in pitch axis
pub note_axis: RwLock<FixedAxis<usize>>,
/// Cursor/scroll/zoom in time axis
pub time_axis: RwLock<ScaledAxis<usize>>,
/// Whether this widget is focused
pub focused: bool,
/// Whether note enter mode is enabled
pub entered: bool,
/// Display mode
pub mode: bool,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub now: Arc<Pulse>,
/// Width of notes area at last render
pub width: AtomicUsize,
/// Height of notes area at last render
pub height: AtomicUsize,
}
/// Phrase player.
pub struct PhrasePlayer {
/// Global timebase
pub clock: Arc<TransportTime>,
/// Start time and phrase being played
pub phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
pub next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_inputs: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outputs: Vec<Port<MidiOut>>,
/// MIDI output buffer
pub midi_note: Vec<u8>,
/// MIDI output buffer
pub midi_chunk: Vec<Vec<Vec<u8>>>,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
}
/// Displays and edits phrase length.
pub struct PhraseLength<E: Engine> {
_engine: PhantomData<E>,
/// 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>,
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone)] pub enum PhraseLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
/// Focus layout of sequencer app
impl<E: Engine> FocusGrid for Sequencer<E> {
type Item = SequencerFocus;
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
fn layout (&self) -> &[&[SequencerFocus]] { &[
&[SequencerFocus::Transport],
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
] }
fn focus_enter (&mut self) { self.entered = true }
fn focus_exit (&mut self) { self.entered = false }
fn entered (&self) -> Option<Self::Item> {
if self.entered { Some(self.focused()) } else { None }
}
fn update_focus (&mut self) {
let focused = self.focused();
if let Some(transport) = self.transport.as_ref() {
transport.write().unwrap().focused = focused == SequencerFocus::Transport
}
self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool;
self.editor.focused = focused == SequencerFocus::PhraseEditor;
}
}
impl<E: Engine> PhrasePool<E> {
pub fn new () -> Self {
Self {
_engine: Default::default(),
scroll: 0,
phrase: 0,
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
mode: None,
focused: false,
entered: false,
}
}
pub fn len (&self) -> usize { self.phrases.len() }
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> { &self.phrases[self.phrase] }
pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) }
pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) }
pub fn index_before (&self, index: usize) -> usize {
index.overflowing_sub(1).0.min(self.len() - 1)
}
pub fn index_after (&self, index: usize) -> usize {
(index + 1) % self.len()
}
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
for i in 0..self.phrases.len() {
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
}
return None
}
fn new_phrase (name: Option<&str>, color: Option<ItemColorTriplet>) -> Arc<RwLock<Phrase>> {
Arc::new(RwLock::new(Phrase::new(
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
)))
}
pub fn delete_selected (&mut self) {
if self.phrase > 0 {
self.phrases.remove(self.phrase);
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
}
}
pub fn append_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.push(Self::new_phrase(name, color));
self.phrase = self.phrases.len() - 1;
}
pub fn insert_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
self.phrase += 1;
}
pub fn insert_dup (&mut self) {
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
self.phrase += 1;
}
pub fn randomize_color (&mut self) {
let mut phrase = self.phrases[self.phrase].write().unwrap();
phrase.color = ItemColorTriplet::random();
}
pub fn begin_rename (&mut self) {
self.mode = Some(PhrasePoolMode::Rename(
self.phrase,
self.phrases[self.phrase].read().unwrap().name.clone()
));
}
pub fn begin_length (&mut self) {
self.mode = Some(PhrasePoolMode::Length(
self.phrase,
self.phrases[self.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
));
}
pub fn move_up (&mut self) {
if self.phrase > 1 {
self.phrases.swap(self.phrase - 1, self.phrase);
self.phrase -= 1;
}
}
pub fn move_down (&mut self) {
if self.phrase < self.phrases.len().saturating_sub(1) {
self.phrases.swap(self.phrase + 1, self.phrase);
self.phrase += 1;
}
}
}
impl<E: Engine> PhraseEditor<E> {
pub fn new () -> Self {
Self {
_engine: Default::default(),
phrase: None,
note_len: 24,
notes_in: Arc::new(RwLock::new([false;128])),
notes_out: Arc::new(RwLock::new([false;128])),
keys: keys_vert(),
buffer: Default::default(),
focused: false,
entered: false,
mode: false,
now: Arc::new(0.into()),
width: 0.into(),
height: 0.into(),
note_axis: RwLock::new(FixedAxis {
start: 12,
point: Some(36),
clamp: Some(127)
}),
time_axis: RwLock::new(ScaledAxis {
start: 00,
point: Some(00),
clamp: Some(000),
scale: 24
}),
}
}
pub fn note_cursor_inc (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
}
pub fn note_cursor_dec (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
}
pub fn note_page_up (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
}
pub fn note_page_down (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
}
pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); }
pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); }
pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) }
pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) }
pub fn time_cursor_advance (&self) {
let point = self.time_axis.read().unwrap().point;
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length;
self.time_axis.write().unwrap().point = point.map(forward);
}
pub fn time_cursor_inc (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().point_inc(scale);
}
pub fn time_cursor_dec (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().point_dec(scale);
}
pub fn time_scroll_inc (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().start_inc(scale);
}
pub fn time_scroll_dec (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().start_dec(scale);
}
pub fn time_zoom_in (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().scale = prev_note_length(scale)
}
pub fn time_zoom_out (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().scale = next_note_length(scale)
}
}
impl Phrase {
pub fn new (
name: impl AsRef<str>,
loop_on: bool,
length: usize,
notes: Option<PhraseData>,
color: Option<ItemColorTriplet>,
) -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
name: name.as_ref().to_string(),
ppq: PPQ,
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
loop_on,
loop_start: 0,
loop_length: length,
percussive: true,
color: color.unwrap_or_else(ItemColorTriplet::random)
}
}
pub fn duplicate (&self) -> Self {
let mut clone = self.clone();
clone.uuid = uuid::Uuid::new_v4();
clone
}
pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; }
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
if pulse >= self.length { panic!("extend phrase first") }
self.notes[pulse].push(message);
}
/// Check if a range `start..end` contains MIDI Note On `k`
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
//panic!("{:?} {start} {end}", &self);
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
for event in events.iter() {
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
}
}
return false
}
}
impl Default for Phrase {
fn default () -> Self {
Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into()))
}
}
impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
impl Eq for Phrase {}
impl PhrasePlayer {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
clock: &Arc<TransportTime>,
name: &str
) -> Usually<Self> {
let jack = jack.read().unwrap();
Ok(Self {
clock: clock.clone(),
phrase: None,
next_phrase: None,
notes_in: Arc::new(RwLock::new([false;128])),
notes_out: Arc::new(RwLock::new([false;128])),
monitoring: false,
recording: false,
overdub: true,
reset: true,
midi_note: Vec::with_capacity(8),
midi_chunk: vec![Vec::with_capacity(16);16384],
midi_outputs: vec![
jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
],
midi_inputs: vec![
jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
],
})
}
pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; }
pub fn toggle_record (&mut self) { self.recording = !self.recording; }
pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; }
pub fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
let start = self.clock.next_launch_pulse();
self.next_phrase = Some((
Instant::from_pulse(&self.clock.timebase(), start as f64),
phrase.map(|p|p.clone())
));
self.reset = true;
}
pub fn pulses_since_start (&self) -> Option<f64> {
if let Some((started, Some(_))) = self.phrase.as_ref() {
Some(self.clock.current.pulse.get() - started.pulse.get())
} else {
None
}
}
}
impl<E: Engine> PhraseLength<E> {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
}
impl PhraseLengthFocus {
pub fn next (&mut self) {
*self = match self {
Self::Bar => Self::Beat,
Self::Beat => Self::Tick,
Self::Tick => Self::Bar,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::Bar => Self::Tick,
Self::Beat => Self::Bar,
Self::Tick => Self::Beat,
}
}
}

View file

@ -1,6 +0,0 @@
//! Multi-track mixer
include!("lib.rs");
pub fn main () -> Usually<()> {
Tui::run(Arc::new(RwLock::new(crate::Track::new("")?)))?;
Ok(())
}

View file

@ -1,81 +0,0 @@
use crate::*;
/// Stores and displays time-related state.
#[derive(Debug)]
pub struct TransportView<E: Engine> {
_engine: PhantomData<E>,
state: TransportToolbar,
focused: bool,
focus: TransportFocus,
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, PartialEq)]
pub enum TransportFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl<E: Engine> TransportView<E> {
pub fn new (jack: &Arc<RwLock<JackClient>>, clock: Option<&Arc<TransportTime>>) -> Self {
Self {
_engine: Default::default(),
focused: false,
focus: TransportFocus::PlayPause,
state: TransportToolbar {
metronome: false,
transport: jack.read().unwrap().transport(),
jack: jack.clone(),
clock: match clock {
Some(clock) => clock.clone(),
None => {
let timebase = Arc::new(Timebase::default());
Arc::new(TransportTime {
playing: Some(TransportState::Stopped).into(),
quant: 24.into(),
sync: (timebase.ppq.get() * 4.).into(),
current: Instant::default(),
started: None.into(),
})
}
},
}
}
}
pub fn toggle_play (&mut self) -> Usually<()> {
let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet");
let playing = match playing {
TransportState::Stopped => {
self.transport.start()?;
Some(TransportState::Starting)
},
_ => {
self.transport.stop()?;
self.transport.locate(0)?;
Some(TransportState::Stopped)
},
};
*self.clock.playing.write().unwrap() = playing;
Ok(())
}
}
impl TransportFocus {
pub fn next (&mut self) {
*self = match self {
Self::PlayPause => Self::Bpm,
Self::Bpm => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::Clock,
Self::Clock => Self::PlayPause,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::PlayPause => Self::Clock,
Self::Bpm => Self::PlayPause,
Self::Quant => Self::Bpm,
Self::Sync => Self::Quant,
Self::Clock => Self::Sync,
}
}
}

View file

@ -811,3 +811,56 @@ impl Scene {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}
/// Layout for standalone arranger app.
impl Content for Arranger<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.arrangement.focused;
let border_bg = Arranger::<Tui>::border_bg();
let border_fg = Arranger::<Tui>::border_fg(focused);
let title_fg = Arranger::<Tui>::title_fg(focused);
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
let entered = if self.arrangement.entered { "" } else { " " };
Split::down(
1,
row!(menu in self.menu.menus.iter() => {
row!(" ", menu.title.as_str(), " ")
}),
Split::up(
1,
widget(&self.status),
Split::up(
1,
widget(&self.transport),
Split::down(
self.arrangement_split,
lay!(
widget(&self.arrangement).grow_y(1).border(border),
widget(&self.arrangement.size),
widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1),
),
Split::right(
self.phrases_split,
self.phrases.clone(),
widget(&self.editor),
)
)
)
)
)
}
}
impl Content for Arrangement<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move |add|{
match self.mode {
ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) },
ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) },
}?;
add(&self.size)
})
}
}

View file

@ -1,5 +1,15 @@
use crate::*;
#[derive(Clone)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
Arrangement(ArrangementCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
/// Handle top-level events in standalone arranger.
impl Handle<Tui> for Arranger<Tui> {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
@ -159,3 +169,103 @@ impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
//Ok(Some(true))
//}
//}
#[derive(Clone)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
Arrangement(ArrangementCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
#[derive(Clone)]
pub enum ArrangementCommand {
New,
Load,
Save,
ToggleViewMode,
Delete,
Activate,
Increment,
Decrement,
ZoomIn,
ZoomOut,
MoveBack,
MoveForward,
RandomColor,
Put,
Get,
AddScene,
AddTrack,
ToggleLoop,
GoUp,
GoDown,
GoLeft,
GoRight,
Edit(Option<Arc<RwLock<Phrase>>>),
}
impl<E: Engine> Command<Arranger<E>> for ArrangerCommand {
fn execute (self, state: &mut Arranger<E>) -> Perhaps<Self> {
let undo = match self {
Self::Focus(cmd) => {
delegate(cmd, Self::Focus, state)
},
Self::Phrases(cmd) => {
delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
},
Self::Editor(cmd) => {
delegate(cmd, Self::Editor, &mut state.editor)
},
Self::Arrangement(cmd) => {
delegate(cmd, Self::Arrangement, &mut state.arrangement)
},
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
} else {
Ok(None)
},
Self::EditPhrase(phrase) => {
state.editor.phrase = phrase.clone();
state.focus(ArrangerFocus::PhraseEditor);
state.focus_enter();
Ok(None)
}
}?;
state.show_phrase();
state.update_status();
return Ok(undo);
}
}
impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
fn execute (self, state: &mut Arrangement<E>) -> Perhaps<Self> {
use ArrangementCommand::*;
match self {
New => todo!(),
Load => todo!(),
Save => todo!(),
Edit(phrase) => { state.phrase = phrase.clone() },
ToggleViewMode => { state.mode.to_next(); },
Delete => { state.delete(); },
Activate => { state.activate(); },
Increment => { state.increment(); },
Decrement => { state.decrement(); },
ZoomIn => { state.zoom_in(); },
ZoomOut => { state.zoom_out(); },
MoveBack => { state.move_back(); },
MoveForward => { state.move_forward(); },
RandomColor => { state.randomize_color(); },
Put => { state.phrase_put(); },
Get => { state.phrase_get(); },
AddScene => { state.scene_add(None, None)?; },
AddTrack => { state.track_add(None, None)?; },
ToggleLoop => { state.toggle_loop() },
GoUp => { state.go_up() },
GoDown => { state.go_down() },
GoLeft => { state.go_left() },
GoRight => { state.go_right() },
};
Ok(None)
}
}

View file

@ -88,8 +88,6 @@ impl<E: Engine> Track<E> {
//}
}
pub struct TrackView<'a, E: Engine> {
pub chain: Option<&'a Track<E>>,
pub direction: Direction,
@ -131,3 +129,40 @@ impl<'a> Widget for TrackView<'a, Tui> {
//}
}
}
impl Content for Mixer<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::right(|add| {
for channel in self.tracks.iter() {
add(channel)?;
}
Ok(())
})
}
}
impl Content for Track<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
TrackView {
chain: Some(&self),
direction: tek_core::Direction::Right,
focused: true,
entered: true,
//pub channels: u8,
//pub input_ports: Vec<Port<AudioIn>>,
//pub pre_gain_meter: f64,
//pub gain: f64,
//pub insert_ports: Vec<Port<AudioOut>>,
//pub return_ports: Vec<Port<AudioIn>>,
//pub post_gain_meter: f64,
//pub post_insert_meter: f64,
//pub level: f64,
//pub pan: f64,
//pub output_ports: Vec<Port<AudioOut>>,
//pub post_fader_meter: f64,
//pub route: String,
}
}
}

View file

@ -1,4 +1,36 @@
use crate::*;
/// A plugin device.
pub struct Plugin<E> {
_engine: PhantomData<E>,
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub path: Option<String>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub ports: JackPorts,
}
impl<E> Plugin<E> {
/// Create a plugin host device.
pub fn new (
jack: &Arc<RwLock<JackClient>>,
name: &str,
) -> Usually<Self> {
Ok(Self {
_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
ports: JackPorts::default()
})
}
}
impl Widget for Plugin<Tui> {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {

View file

@ -56,21 +56,3 @@ impl ApplicationHandler for LV2PluginUI {
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}
pub fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
let counts = plugin.port_counts();
let mut jack = Jack::new(name)?;
for i in 0..counts.atom_sequence_inputs {
jack = jack.midi_in(&format!("midi-in-{i}"))
}
for i in 0..counts.atom_sequence_outputs {
jack = jack.midi_out(&format!("midi-out-{i}"));
}
for i in 0..counts.audio_inputs {
jack = jack.audio_in(&format!("audio-in-{i}"));
}
for i in 0..counts.audio_outputs {
jack = jack.audio_out(&format!("audio-out-{i}"));
}
Ok(jack)
}

View file

@ -117,20 +117,6 @@ pub struct Sample {
pub rate: Option<usize>,
}
impl Sample {
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self { name: name.to_string(), start, end, channels, rate: None }
}
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
}
/// Load sample from WAV and assign to MIDI note.
#[macro_export] macro_rules! sample {
@ -412,3 +398,100 @@ impl Iterator for Voice {
None
}
}
impl Widget for Sampler<Tui> {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
tui_render_sampler(self, to)
}
}
pub fn tui_render_sampler (sampler: &Sampler<Tui>, to: &mut TuiOutput) -> Usually<()> {
let [x, y, _, height] = to.area();
let style = Style::default().gray();
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
let mut width = title.len() + 2;
let mut y1 = 1;
let mut j = 0;
for (note, sample) in sampler.mapped.iter()
.map(|(note, sample)|(Some(note), sample))
.chain(sampler.unmapped.iter().map(|sample|(None, sample)))
{
if y1 >= height {
break
}
let active = j == sampler.cursor.0;
width = width.max(
draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
);
y1 = y1 + 1;
j = j + 1;
}
let height = ((2 + y1) as u16).min(height);
//Ok(Some([x, y, (width as u16).min(to.area().w()), height]))
Ok(())
}
fn draw_sample (
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
impl Widget for AddSampleModal {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
//Align::Center(()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
todo!()
//let area = to.area();
//to.make_dim();
//let area = center_box(
//area,
//64.max(area.w().saturating_sub(8)),
//20.max(area.w().saturating_sub(8)),
//);
//to.fill_fg(area, Color::Reset);
//to.fill_bg(area, Nord::bg_lo(true, true));
//to.fill_char(area, ' ');
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
//for (i, (is_dir, name)) in self.subdirs.iter()
//.map(|path|(true, path))
//.chain(self.files.iter().map(|path|(false, path)))
//.enumerate()
//.skip(self.offset)
//{
//if i >= area.h() as usize - 4 {
//break
//}
//let t = if is_dir { "" } else { "" };
//let line = format!("{t} {}", name.to_string_lossy());
//let line = &line[..line.len().min(area.w() as usize - 4)];
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
//Style::default().green()
//} else {
//Style::default().white()
//}))?;
//}
//Lozenge(Style::default()).draw(to)
}
}

View file

@ -1,4 +1,419 @@
use crate::*;
use std::cmp::PartialEq;
/// MIDI message structural
pub type PhraseData = Vec<Vec<MidiMessage>>;
/// MIDI message serialized
pub type PhraseMessage = Vec<u8>;
/// Collection of serialized MIDI messages
pub type PhraseChunk = [Vec<PhraseMessage>];
/// Root level object for standalone `tek_sequencer`
pub struct Sequencer<E: Engine> {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
/// Controls the JACK transport.
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
/// Global timebase
pub clock: Arc<TransportTime>,
/// Pool of all phrases available to the sequencer
pub phrases: Arc<RwLock<PhrasePool<E>>>,
/// Phrase editor view
pub editor: PhraseEditor<E>,
/// Phrase player
pub player: PhrasePlayer,
/// Which view is focused
pub focus_cursor: (usize, usize),
/// Whether the currently focused item is entered
pub entered: bool,
}
/// Sections in the sequencer app that may be focused
#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus {
/// The transport (toolbar) is focused
Transport,
/// The phrase list (pool) is focused
PhrasePool,
/// The phrase editor (sequencer) is focused
PhraseEditor,
}
/// Status bar for sequencer app
pub enum SequencerStatusBar {
Transport,
PhrasePool,
PhraseEditor,
}
/// Modes for phrase pool
pub enum PhrasePoolMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
}
/// A MIDI sequence.
#[derive(Debug, Clone)] pub struct Phrase {
pub uuid: uuid::Uuid,
/// Name of phrase
pub name: String,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of phrase in pulses
pub length: usize,
/// Notes in phrase
pub notes: PhraseData,
/// Whether to loop the phrase or play it once
pub loop_on: bool,
/// Start of loop
pub loop_start: usize,
/// Length of loop
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of phrase
pub color: ItemColorTriplet,
}
/// Contains state for viewing and editing a phrase
pub struct PhraseEditor<E: Engine> {
_engine: PhantomData<E>,
/// Phrase being played
pub phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub note_len: usize,
/// The full piano keys are rendered to this buffer
pub keys: Buffer,
/// The full piano roll is rendered to this buffer
pub buffer: BigBuffer,
/// Cursor/scroll/zoom in pitch axis
pub note_axis: RwLock<FixedAxis<usize>>,
/// Cursor/scroll/zoom in time axis
pub time_axis: RwLock<ScaledAxis<usize>>,
/// Whether this widget is focused
pub focused: bool,
/// Whether note enter mode is enabled
pub entered: bool,
/// Display mode
pub mode: bool,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub now: Arc<Pulse>,
/// Width of notes area at last render
pub width: AtomicUsize,
/// Height of notes area at last render
pub height: AtomicUsize,
}
/// Phrase player.
pub struct PhrasePlayer {
/// Global timebase
pub clock: Arc<TransportTime>,
/// Start time and phrase being played
pub phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
pub next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_inputs: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outputs: Vec<Port<MidiOut>>,
/// MIDI output buffer
pub midi_note: Vec<u8>,
/// MIDI output buffer
pub midi_chunk: Vec<Vec<Vec<u8>>>,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
}
/// Displays and edits phrase length.
pub struct PhraseLength<E: Engine> {
_engine: PhantomData<E>,
/// 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>,
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone)] pub enum PhraseLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
/// Focus layout of sequencer app
impl<E: Engine> FocusGrid for Sequencer<E> {
type Item = SequencerFocus;
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
fn layout (&self) -> &[&[SequencerFocus]] { &[
&[SequencerFocus::Transport],
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
] }
fn focus_enter (&mut self) { self.entered = true }
fn focus_exit (&mut self) { self.entered = false }
fn entered (&self) -> Option<Self::Item> {
if self.entered { Some(self.focused()) } else { None }
}
fn update_focus (&mut self) {
let focused = self.focused();
if let Some(transport) = self.transport.as_ref() {
transport.write().unwrap().focused = focused == SequencerFocus::Transport
}
self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool;
self.editor.focused = focused == SequencerFocus::PhraseEditor;
}
}
impl<E: Engine> PhrasePool<E> {
pub fn new () -> Self {
Self {
_engine: Default::default(),
scroll: 0,
phrase: 0,
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
mode: None,
focused: false,
entered: false,
}
}
pub fn len (&self) -> usize { self.phrases.len() }
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> { &self.phrases[self.phrase] }
pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) }
pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) }
pub fn index_before (&self, index: usize) -> usize {
index.overflowing_sub(1).0.min(self.len() - 1)
}
pub fn index_after (&self, index: usize) -> usize {
(index + 1) % self.len()
}
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
for i in 0..self.phrases.len() {
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
}
return None
}
fn new_phrase (name: Option<&str>, color: Option<ItemColorTriplet>) -> Arc<RwLock<Phrase>> {
Arc::new(RwLock::new(Phrase::new(
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
)))
}
pub fn delete_selected (&mut self) {
if self.phrase > 0 {
self.phrases.remove(self.phrase);
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
}
}
pub fn append_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.push(Self::new_phrase(name, color));
self.phrase = self.phrases.len() - 1;
}
pub fn insert_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
self.phrase += 1;
}
pub fn insert_dup (&mut self) {
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
self.phrase += 1;
}
pub fn randomize_color (&mut self) {
let mut phrase = self.phrases[self.phrase].write().unwrap();
phrase.color = ItemColorTriplet::random();
}
pub fn begin_rename (&mut self) {
self.mode = Some(PhrasePoolMode::Rename(
self.phrase,
self.phrases[self.phrase].read().unwrap().name.clone()
));
}
pub fn begin_length (&mut self) {
self.mode = Some(PhrasePoolMode::Length(
self.phrase,
self.phrases[self.phrase].read().unwrap().length,
PhraseLengthFocus::Bar
));
}
pub fn move_up (&mut self) {
if self.phrase > 1 {
self.phrases.swap(self.phrase - 1, self.phrase);
self.phrase -= 1;
}
}
pub fn move_down (&mut self) {
if self.phrase < self.phrases.len().saturating_sub(1) {
self.phrases.swap(self.phrase + 1, self.phrase);
self.phrase += 1;
}
}
}
impl<E: Engine> PhraseEditor<E> {
pub fn new () -> Self {
Self {
_engine: Default::default(),
phrase: None,
note_len: 24,
notes_in: Arc::new(RwLock::new([false;128])),
notes_out: Arc::new(RwLock::new([false;128])),
keys: keys_vert(),
buffer: Default::default(),
focused: false,
entered: false,
mode: false,
now: Arc::new(0.into()),
width: 0.into(),
height: 0.into(),
note_axis: RwLock::new(FixedAxis {
start: 12,
point: Some(36),
clamp: Some(127)
}),
time_axis: RwLock::new(ScaledAxis {
start: 00,
point: Some(00),
clamp: Some(000),
scale: 24
}),
}
}
pub fn note_cursor_inc (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.point_dec(1);
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
}
pub fn note_cursor_dec (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.point_inc(1);
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
}
pub fn note_page_up (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.start_dec(3);
axis.point_dec(3);
}
pub fn note_page_down (&self) {
let mut axis = self.note_axis.write().unwrap();
axis.start_inc(3);
axis.point_inc(3);
}
pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); }
pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); }
pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) }
pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) }
pub fn time_cursor_advance (&self) {
let point = self.time_axis.read().unwrap().point;
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
let forward = |time|(time + self.note_len) % length;
self.time_axis.write().unwrap().point = point.map(forward);
}
pub fn time_cursor_inc (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().point_inc(scale);
}
pub fn time_cursor_dec (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().point_dec(scale);
}
pub fn time_scroll_inc (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().start_inc(scale);
}
pub fn time_scroll_dec (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().start_dec(scale);
}
pub fn time_zoom_in (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().scale = prev_note_length(scale)
}
pub fn time_zoom_out (&self) {
let scale = self.time_axis.read().unwrap().scale;
self.time_axis.write().unwrap().scale = next_note_length(scale)
}
}
impl PhrasePlayer {
pub fn new (
jack: &Arc<RwLock<JackClient>>,
clock: &Arc<TransportTime>,
name: &str
) -> Usually<Self> {
let jack = jack.read().unwrap();
Ok(Self {
clock: clock.clone(),
phrase: None,
next_phrase: None,
notes_in: Arc::new(RwLock::new([false;128])),
notes_out: Arc::new(RwLock::new([false;128])),
monitoring: false,
recording: false,
overdub: true,
reset: true,
midi_note: Vec::with_capacity(8),
midi_chunk: vec![Vec::with_capacity(16);16384],
midi_outputs: vec![
jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
],
midi_inputs: vec![
jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
],
})
}
pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; }
pub fn toggle_record (&mut self) { self.recording = !self.recording; }
pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; }
pub fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
let start = self.clock.next_launch_pulse();
self.next_phrase = Some((
Instant::from_pulse(&self.clock.timebase(), start as f64),
phrase.map(|p|p.clone())
));
self.reset = true;
}
pub fn pulses_since_start (&self) -> Option<f64> {
if let Some((started, Some(_))) = self.phrase.as_ref() {
Some(self.clock.current.pulse.get() - started.pulse.get())
} else {
None
}
}
}
impl<E: Engine> PhraseLength<E> {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
}
impl PhraseLengthFocus {
pub fn next (&mut self) {
*self = match self {
Self::Bar => Self::Beat,
Self::Beat => Self::Tick,
Self::Tick => Self::Bar,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::Bar => Self::Tick,
Self::Beat => Self::Bar,
Self::Tick => Self::Beat,
}
}
}
impl Content for Sequencer<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {

View file

@ -1,4 +1,84 @@
use crate::*;
/// Stores and displays time-related state.
#[derive(Debug)]
pub struct TransportView<E: Engine> {
_engine: PhantomData<E>,
state: TransportToolbar,
focused: bool,
focus: TransportFocus,
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, PartialEq)]
pub enum TransportFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl<E: Engine> TransportView<E> {
pub fn new (jack: &Arc<RwLock<JackClient>>, clock: Option<&Arc<TransportTime>>) -> Self {
Self {
_engine: Default::default(),
focused: false,
focus: TransportFocus::PlayPause,
state: TransportToolbar {
metronome: false,
transport: jack.read().unwrap().transport(),
jack: jack.clone(),
clock: match clock {
Some(clock) => clock.clone(),
None => {
let timebase = Arc::new(Timebase::default());
Arc::new(TransportTime {
playing: Some(TransportState::Stopped).into(),
quant: 24.into(),
sync: (timebase.ppq.get() * 4.).into(),
current: Instant::default(),
started: None.into(),
})
}
},
}
}
}
pub fn toggle_play (&mut self) -> Usually<()> {
let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet");
let playing = match playing {
TransportState::Stopped => {
self.transport.start()?;
Some(TransportState::Starting)
},
_ => {
self.transport.stop()?;
self.transport.locate(0)?;
Some(TransportState::Stopped)
},
};
*self.clock.playing.write().unwrap() = playing;
Ok(())
}
}
impl TransportFocus {
pub fn next (&mut self) {
*self = match self {
Self::PlayPause => Self::Bpm,
Self::Bpm => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::Clock,
Self::Clock => Self::PlayPause,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::PlayPause => Self::Clock,
Self::Bpm => Self::PlayPause,
Self::Quant => Self::Bpm,
Self::Sync => Self::Quant,
Self::Clock => Self::Sync,
}
}
}
impl Content for TransportToolbar<Tui> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {