wip: running interface in separate or combined mode

also disassociating render functions from state structs
This commit is contained in:
🪞👃🪞 2024-05-28 22:13:20 +03:00
parent f9218e887a
commit d6bf840a1f
31 changed files with 905 additions and 532 deletions

22
src/cli.rs Normal file
View file

@ -0,0 +1,22 @@
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
/// Launch or control a master transport
Transport,
/// Launch or control a mixer
Mixer,
/// Launch or control a looper
Looper,
/// Launch or control a sampler
Sampler,
/// Launch or control a sequencer
Sequencer,
}

View file

@ -1,54 +1,19 @@
use std::error::Error;
use std::io::Write;
mod handle;
mod jack;
mod render;
pub use self::handle::*;
pub use self::jack::*;
pub use self::render::*;
use crate::prelude::*;
pub struct Looper {
cols: u16,
rows: u16,
}
pub struct Looper;
const ACTIONS: [(&'static str, &'static str);1] = [
("Ins/Del", "Add/remove loop"),
];
impl Looper {
pub fn run_tui () -> Result<(), Box<dyn Error>> {
let mut app = Self { cols: 0, rows: 0 };
let sleep = std::time::Duration::from_millis(16);
let mut stdout = std::io::stdout();
loop {
app.render(&mut stdout)?;
std::thread::sleep(sleep);
}
}
fn render (&mut self, stdout: &mut std::io::Stdout) -> Result<(), Box<dyn Error>> {
use crossterm::{*, style::{*, Stylize}};
let (cols, rows) = terminal::size()?;
if cols != self.cols || rows != self.rows {
self.cols = cols;
self.rows = rows;
stdout
.queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::Hide)?
.queue(cursor::MoveTo(1, 0))?
.queue(PrintStyledContent("[Up/Down]".yellow().bold()))?
.queue(cursor::MoveTo(1, 1))?
.queue(PrintStyledContent("Select sequence".yellow()))?
.queue(cursor::MoveTo(18, 0))?
.queue(PrintStyledContent("[Left/Right]".yellow().bold()))?
.queue(cursor::MoveTo(18, 1))?
.queue(PrintStyledContent("Select parameter".yellow()))?
.queue(cursor::MoveTo(36, 0))?
.queue(PrintStyledContent("[Insert/Delete]".yellow().bold()))?
.queue(cursor::MoveTo(36, 1))?
.queue(PrintStyledContent("Add/remove sequence".yellow()))?
.queue(cursor::MoveTo(0, 3))?.queue(Print(" Name Input Length Route"))?
.queue(cursor::MoveTo(0, 4))?.queue(PrintStyledContent(" Metronome [ ] ████ Track 1".bold()))?
.queue(cursor::MoveTo(0, 5))?.queue(PrintStyledContent(" Loop 1 [ ] ████ Track 1".bold()))?
.queue(cursor::MoveTo(0, 6))?.queue(PrintStyledContent(" Loop 2 [ ] ████████ Track 2".bold()))?
.queue(cursor::MoveTo(0, 7))?.queue(PrintStyledContent(" Loop 3 [ ] ████████ Track 3".bold()))?
.flush()?;
}
Ok(())
pub fn new () -> Result<Self, Box<dyn Error>> {
Ok(Self)
}
}

2
src/looper/handle.rs Normal file
View file

@ -0,0 +1,2 @@
use crate::prelude::*;
use super::Looper;

0
src/looper/jack.rs Normal file
View file

20
src/looper/render.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::prelude::*;
use super::{Looper, ACTIONS};
pub fn render (
state: &mut Looper,
stdout: &mut std::io::Stdout,
mut offset: (u16, u16),
) -> Result<(), Box<dyn Error>> {
let (w, h) = crate::render::render_toolbar_vertical(stdout, offset, &ACTIONS)?;
offset.0 = offset.0 + w + 2;
let move_to = |col, row| MoveTo(offset.0 + col, offset.1 + row);
stdout
.queue(move_to(0, 0))?.queue(Print(" Name Input Length Route"))?
.queue(move_to(0, 1))?.queue(PrintStyledContent(" Metronome [ ] ████ Track 1".bold()))?
.queue(move_to(0, 2))?.queue(PrintStyledContent(" Loop 1 [ ] ████ Track 1".bold()))?
.queue(move_to(0, 3))?.queue(PrintStyledContent(" Loop 2 [ ] ████████ Track 2".bold()))?
.queue(move_to(0, 4))?.queue(PrintStyledContent(" Loop 3 [ ] ████████ Track 3".bold()))?
.flush()?;
Ok(())
}

View file

@ -5,43 +5,146 @@ extern crate crossterm;
use clap::{Parser, Subcommand};
use std::error::Error;
//pub mod sequence;
pub mod cli;
pub mod prelude;
pub mod engine;
pub mod transport;
pub mod mixer;
pub mod looper;
pub mod sampler;
pub mod sequencer;
pub mod render;
use crate::prelude::*;
fn main () -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
match cli.command {
Command::Transport =>
crate::transport::Transport::run_tui(),
Command::Mixer =>
crate::mixer::Mixer::run_tui(),
Command::Looper =>
crate::looper::Looper::run_tui(),
Command::Sampler =>
crate::sampler::Sampler::run_tui(),
let cli = cli::Cli::parse();
if let Some(command) = cli.command {
match command {
cli::Command::Transport => main_loop(
&mut transport::Transport::new()?,
transport::render
),
cli::Command::Mixer => main_loop(
&mut mixer::Mixer::new()?,
mixer::render
),
cli::Command::Looper => main_loop(
&mut looper::Looper::new()?,
looper::render
),
cli::Command::Sampler => main_loop(
&mut sampler::Sampler::new()?,
sampler::render
),
cli::Command::Sequencer => main_loop(
&mut sequencer::Sequencer::new()?,
sequencer::render
),
}
} else {
main_loop(&mut (
transport::Transport::new()?,
mixer::Mixer::new()?,
looper::Looper::new()?,
sampler::Sampler::new()?,
sequencer::Sequencer::new()?,
), |(transport, mixer, looper, sampler, sequencer), stdout, offset| {
transport::render(transport, stdout, (1, 1))?;
render::render_box(stdout, 28, 0, 60, 6, false)?;
mixer::render(mixer, stdout, (1, 10))?;
render::render_box(stdout, 18, 9, 62, 9, false)?;
looper::render(looper, stdout, (1, 20))?;
render::render_box(stdout, 17, 19, 50, 6, false)?;
//sampler::render(sampler, stdout, (1, 46))?;
//sequencer::render(sequencer, stdout, (1, 66))?;
Ok(())
})
}
}
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Command
pub fn main_loop <T> (
state: &mut T,
mut render: impl FnMut(&mut T, &mut Stdout, (u16, u16)) -> Result<(), Box<dyn Error>>
) -> Result<(), Box<dyn Error>> {
let sleep = std::time::Duration::from_millis(16);
let mut stdout = std::io::stdout();
loop {
stdout.queue(Clear(ClearType::All))?.queue(Hide)?;
render(state, &mut stdout, (0, 0))?;
stdout.flush()?;
std::thread::sleep(sleep);
}
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
/// Control the master transport
Transport,
/// Control the mixer
Mixer,
/// Control the looper
Looper,
/// Control the sampler
Sampler,
}
//pub fn run_tui () -> Result<(), Box<dyn Error>> {
//let mut stdout = stdout();
//let mut app = Mixer::new()?;
//let sleep = std::time::Duration::from_millis(16);
//crossterm::terminal::enable_raw_mode()?;
//let (tx, input) = channel::<crossterm::event::Event>();
//let exited = Arc::new(AtomicBool::new(false));
//let exit_input_thread = exited.clone();
//spawn(move || {
//loop {
//// Exit if flag is set
//if exit_input_thread.fetch_and(true, Ordering::Relaxed) {
//break
//}
//// Listen for events and send them to the main thread
//if crossterm::event::poll(Duration::from_millis(100)).is_ok() {
//if tx.send(crossterm::event::read().unwrap()).is_err() {
//break
//}
//}
//}
//});
//loop {
//render(&mut app, &mut stdout, (0, 0))?;
//handle(&mut app, input.recv()?)?;
//if app.exit {
//app.stdout.queue(cursor::Show)?.flush()?;
//crossterm::terminal::disable_raw_mode()?;
//break
//}
//}
//Ok(())
//}
//pub fn run_tui () -> Result<(), Box<dyn Error>> {
//let mut app = Self::new()?;
//let mut stdout = std::io::stdout();
//let sleep = std::time::Duration::from_millis(16);
//crossterm::terminal::enable_raw_mode()?;
//let (tx, input) = channel::<crossterm::event::Event>();
//let exited = Arc::new(AtomicBool::new(false));
//// Spawn the input thread
//let exit_input_thread = exited.clone();
//spawn(move || {
//loop {
//// Exit if flag is set
//if exit_input_thread.fetch_and(true, Ordering::Relaxed) {
//break
//}
//// Listen for events and send them to the main thread
//if crossterm::event::poll(Duration::from_millis(100)).is_ok() {
//if tx.send(crossterm::event::read().unwrap()).is_err() {
//break
//}
//}
//}
//});
//loop {
//use crossterm::{*, terminal::{Clear, ClearType}, cursor::Hide};
//stdout.queue(Clear(ClearType::All))?.queue(Hide)?;
//render(&mut app, &mut stdout, (0, 0))?;
//stdout.flush()?;
//handle(&mut app, input.recv()?)?;
//if app.exit {
//stdout.queue(cursor::Hide)?.flush()?;
//crossterm::terminal::disable_raw_mode()?;
//break
//}
//}
//Ok(())
//}

View file

@ -1,3 +1,9 @@
mod handle;
mod jack;
mod render;
pub use self::handle::*;
pub use self::jack::*;
pub use self::render::*;
use crate::prelude::*;
// TODO:
@ -5,17 +11,36 @@ use crate::prelude::*;
// - If one stage clips, all stages after it are marked red
// - If one track clips, all tracks that feed from it are marked red?
const ACTIONS: [(&'static str, &'static str);2] = [
("+/-", "Adjust"),
("Ins/Del", "Add/remove track"),
];
pub struct Mixer {
jack: Jack<Notifications>,
exit: bool,
stdout: Stdout,
cols: u16,
rows: u16,
tracks: Vec<Track>,
selected_track: usize,
selected_column: usize,
}
pub struct Track {
name: String,
channels: u8,
input_ports: Vec<Port<AudioIn>>,
pre_gain_meter: f64,
gain: f64,
insert_ports: Vec<Port<AudioOut>>,
return_ports: Vec<Port<AudioIn>>,
post_gain_meter: f64,
post_insert_meter: f64,
level: f64,
pan: f64,
output_ports: Vec<Port<AudioOut>>,
post_fader_meter: f64,
route: String,
}
impl Mixer {
pub fn new () -> Result<Self, Box<dyn Error>> {
@ -33,9 +58,6 @@ impl Mixer {
)?;
Ok(Self {
exit: false,
stdout: std::io::stdout(),
cols: 0,
rows: 0,
selected_column: 0,
selected_track: 1,
tracks: vec![
@ -51,174 +73,6 @@ impl Mixer {
})
}
pub fn run_tui () -> Result<(), Box<dyn Error>> {
let mut app = Self::new()?;
let sleep = std::time::Duration::from_millis(16);
crossterm::terminal::enable_raw_mode()?;
let (tx, input) = channel::<crossterm::event::Event>();
let exited = Arc::new(AtomicBool::new(false));
// Spawn the input thread
let exit_input_thread = exited.clone();
spawn(move || {
loop {
// Exit if flag is set
if exit_input_thread.fetch_and(true, Ordering::Relaxed) {
break
}
// Listen for events and send them to the main thread
if crossterm::event::poll(Duration::from_millis(100)).is_ok() {
if tx.send(crossterm::event::read().unwrap()).is_err() {
break
}
}
}
});
loop {
app.render()?;
app.handle(input.recv()?)?;
if app.exit {
app.stdout.queue(cursor::Hide)?.flush()?;
crossterm::terminal::disable_raw_mode()?;
break
}
}
Ok(())
}
fn handle (&mut self, event: crossterm::event::Event) -> Result<(), Box<dyn Error>> {
use crossterm::event::{Event, KeyCode, KeyModifiers};
if let Event::Key(event) = event {
match event.code {
KeyCode::Char('c') => {
if event.modifiers == KeyModifiers::CONTROL {
self.exit = true;
}
},
KeyCode::Down => {
self.selected_track = (self.selected_track + 1) % self.tracks.len();
println!("{}", self.selected_track);
},
KeyCode::Up => {
if self.selected_track == 0 {
self.selected_track = self.tracks.len() - 1;
} else {
self.selected_track = self.selected_track - 1;
}
println!("{}", self.selected_track);
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column = self.selected_column - 1;
}
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column = self.selected_column + 1;
}
},
_ => {
println!("{event:?}");
}
}
}
Ok(())
}
fn render (&mut self) -> Result<(), Box<dyn Error>> {
let (cols, rows) = terminal::size()?;
self.cols = cols;
self.rows = rows;
self.stdout.queue(terminal::Clear(terminal::ClearType::All))?;
self.stdout.queue(cursor::Hide)?;
self.render_toolbar()?;
self.render_table()?;
self.render_meters()?;
self.stdout.flush()?;
Ok(())
}
fn render_toolbar (&mut self) -> Result<(), Box<dyn Error>> {
self.stdout
.queue(cursor::MoveTo(1, 0))?
.queue(PrintStyledContent("[Arrows]".yellow().bold()))?
.queue(cursor::MoveTo(1, 1))?
.queue(PrintStyledContent("Navigate".yellow()))?
.queue(cursor::MoveTo(11, 0))?
.queue(PrintStyledContent("[+/-]".yellow().bold()))?
.queue(cursor::MoveTo(11, 1))?
.queue(PrintStyledContent("Adjust value".yellow()))?
.queue(cursor::MoveTo(25, 0))?
.queue(PrintStyledContent("[Ins/Del]".yellow().bold()))?
.queue(cursor::MoveTo(25, 1))?
.queue(PrintStyledContent("Add/remove track".yellow()))?;
Ok(())
}
fn render_table (&mut self) -> Result<(), Box<dyn Error>> {
self.stdout
.queue(cursor::MoveTo(0, 3))?
.queue(Print(
" Name Gain FX1 Pan Level FX2 Route"))?;
for (i, track) in self.tracks.iter().enumerate() {
let row = 4 + i as u16;
for (j, (column, field)) in [
(0, format!(" {:7} ", track.name)),
(12, format!(" {:.1}dB ", track.gain)),
(22, format!(" [ ] ")),
(30, format!(" C ")),
(35, format!(" {:.1}dB ", track.level)),
(45, format!(" [ ] ")),
(51, format!(" {:7} ", track.route)),
].into_iter().enumerate() {
self.stdout.queue(cursor::MoveTo(column, row))?;
if self.selected_track == i && self.selected_column == j {
self.stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
} else {
self.stdout.queue(PrintStyledContent(field.to_string().bold()))?;
}
}
}
Ok(())
}
fn render_meters (&mut self) -> Result<(), Box<dyn Error>> {
for (i, track) in self.tracks.iter().enumerate() {
let row = 4 + i as u16;
self.stdout
.queue(cursor::MoveTo(10, row))?
.queue(PrintStyledContent("".green()))?
.queue(cursor::MoveTo(20, row))?
.queue(PrintStyledContent("".green()))?
.queue(cursor::MoveTo(28, row))?
.queue(PrintStyledContent("".green()))?
.queue(cursor::MoveTo(43, row))?
.queue(PrintStyledContent("".green()))?;
}
Ok(())
}
}
pub struct Track {
name: String,
channels: u8,
input_ports: Vec<Port<AudioIn>>,
pre_gain_meter: f64,
gain: f64,
insert_ports: Vec<Port<AudioOut>>,
return_ports: Vec<Port<AudioIn>>,
post_gain_meter: f64,
post_insert_meter: f64,
level: f64,
pan: f64,
output_ports: Vec<Port<AudioOut>>,
post_fader_meter: f64,
route: String,
}
impl Track {
@ -254,41 +108,3 @@ impl Track {
})
}
}
struct Notifications;
impl NotificationHandler for Notifications {
fn thread_init (&self, _: &Client) {
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
}
fn freewheel (&mut self, _: &Client, is_enabled: bool) {
}
fn sample_rate (&mut self, _: &Client, _: Frames) -> Control {
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) {
}
fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) {
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
Control::Continue
}
fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) {
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}

48
src/mixer/handle.rs Normal file
View file

@ -0,0 +1,48 @@
use crate::prelude::*;
use super::Mixer;
pub fn handle (
state: &mut Mixer,
event: crossterm::event::Event
) -> Result<(), Box<dyn Error>> {
use crossterm::event::{Event, KeyCode, KeyModifiers};
if let Event::Key(event) = event {
match event.code {
KeyCode::Char('c') => {
if event.modifiers == KeyModifiers::CONTROL {
state.exit = true;
}
},
KeyCode::Down => {
state.selected_track = (state.selected_track + 1) % state.tracks.len();
println!("{}", state.selected_track);
},
KeyCode::Up => {
if state.selected_track == 0 {
state.selected_track = state.tracks.len() - 1;
} else {
state.selected_track = state.selected_track - 1;
}
println!("{}", state.selected_track);
},
KeyCode::Left => {
if state.selected_column == 0 {
state.selected_column = 6
} else {
state.selected_column = state.selected_column - 1;
}
},
KeyCode::Right => {
if state.selected_column == 6 {
state.selected_column = 0
} else {
state.selected_column = state.selected_column + 1;
}
},
_ => {
println!("{event:?}");
}
}
}
Ok(())
}

40
src/mixer/jack.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::prelude::*;
pub struct Notifications;
impl NotificationHandler for Notifications {
fn thread_init (&self, _: &Client) {
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
}
fn freewheel (&mut self, _: &Client, is_enabled: bool) {
}
fn sample_rate (&mut self, _: &Client, _: Frames) -> Control {
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) {
}
fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) {
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
Control::Continue
}
fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) {
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}

66
src/mixer/render.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::prelude::*;
use super::{Mixer, ACTIONS};
pub fn render (
state: &mut Mixer,
stdout: &mut Stdout,
mut offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
let (w, h) = crate::render::render_toolbar_vertical(stdout, offset, &ACTIONS)?;
offset.0 = offset.0 + w + 2;
render_table(state, stdout, offset)?;
render_meters(state, stdout, offset)?;
stdout.flush()?;
Ok(())
}
fn render_table (
state: &mut Mixer,
stdout: &mut Stdout,
offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
stdout.queue(
move_to(0, 0)
)?.queue(
Print(" Name Gain FX1 Pan Level FX2 Route")
)?;
for (i, track) in state.tracks.iter().enumerate() {
let row = (i + 1) as u16;
for (j, (column, field)) in [
(0, format!(" {:7} ", track.name)),
(12, format!(" {:.1}dB ", track.gain)),
(22, format!(" [ ] ")),
(30, format!(" C ")),
(35, format!(" {:.1}dB ", track.level)),
(45, format!(" [ ] ")),
(51, format!(" {:7} ", track.route)),
].into_iter().enumerate() {
stdout.queue(move_to(column, row))?;
if state.selected_track == i && state.selected_column == j {
stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
} else {
stdout.queue(PrintStyledContent(field.to_string().bold()))?;
}
}
}
Ok(())
}
fn render_meters (
state: &mut Mixer,
stdout: &mut Stdout,
offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row);
for (i, track) in state.tracks.iter().enumerate() {
let row = (i + 1) as u16;
stdout
.queue(move_to(10, row))?.queue(PrintStyledContent("".green()))?
.queue(move_to(20, row))?.queue(PrintStyledContent("".green()))?
.queue(move_to(28, row))?.queue(PrintStyledContent("".green()))?
.queue(move_to(43, row))?.queue(PrintStyledContent("".green()))?;
}
Ok(())
}

View file

@ -9,8 +9,8 @@ pub use std::sync::{
};
pub use crossterm::{
QueueableCommand,
cursor,
terminal,
cursor::{self, MoveTo, Hide},
terminal::{self, Clear, ClearType},
style::*,
};
pub use jack::{

41
src/render.rs Normal file
View file

@ -0,0 +1,41 @@
use crate::prelude::*;
pub fn render_toolbar_vertical (
stdout: &mut std::io::Stdout,
offset: (u16, u16),
actions: &[(&str, &str)],
) -> Result<(u16, u16), Box<dyn Error>> {
use crossterm::{*, style::{Stylize, PrintStyledContent}, cursor::MoveTo};
let move_to = |col, row| MoveTo(offset.0 + col, offset.1 + row);
let mut x: u16 = 1;
let mut y: u16 = 0;
for (name, description) in actions.iter() {
stdout.queue(move_to(1, y))?.queue(
PrintStyledContent(name.yellow().bold())
)?.queue(move_to(1, y + 1))?.queue(
PrintStyledContent(description.yellow())
)?;
y = y + 3;
x = u16::max(x, usize::max(name.len(), description.len()) as u16);
}
Ok((x, y))
}
pub fn render_box (
stdout: &mut std::io::Stdout,
x: u16,
y: u16,
w: u16,
h: u16,
active: bool
) -> Result<(), Box<dyn Error>> {
let edge: String = std::iter::repeat("").take(w.saturating_sub(2) as usize).collect();
let back: String = std::iter::repeat(" ").take(w.saturating_sub(2) as usize).collect();
stdout.queue(MoveTo(x, y))?.queue(Print(&format!("{edge}")))?;
for row in y+1..y+h {
stdout.queue(MoveTo(x, row))?.queue(Print(""))?;
stdout.queue(MoveTo(x+w-1, row))?.queue(Print(""))?;
}
stdout.queue(MoveTo(x, y+h))?.queue(Print(&format!("{edge}")))?;
Ok(())
}

View file

@ -1,18 +1,20 @@
mod handle;
mod jack;
mod render;
pub use self::handle::*;
pub use self::jack::*;
pub use self::render::*;
use crate::prelude::*;
pub struct Sampler {
jack: Jack<Notifications>,
exit: bool,
stdout: Stdout,
cols: u16,
rows: u16,
samples: Vec<Sample>,
selected_sample: usize,
selected_column: usize,
}
impl Sampler {
pub fn new () -> Result<Self, Box<dyn Error>> {
let (client, status) = Client::new(
"bloop-sampler",
@ -28,9 +30,6 @@ impl Sampler {
)?;
Ok(Self {
exit: false,
stdout: std::io::stdout(),
cols: 0,
rows: 0,
selected_sample: 0,
selected_column: 0,
samples: vec![
@ -40,150 +39,6 @@ impl Sampler {
jack,
})
}
pub fn run_tui () -> Result<(), Box<dyn Error>> {
let mut app = Self::new()?;
let sleep = std::time::Duration::from_millis(16);
crossterm::terminal::enable_raw_mode()?;
let (tx, input) = channel::<crossterm::event::Event>();
let exited = Arc::new(AtomicBool::new(false));
// Spawn the input thread
let exit_input_thread = exited.clone();
spawn(move || {
loop {
// Exit if flag is set
if exit_input_thread.fetch_and(true, Ordering::Relaxed) {
break
}
// Listen for events and send them to the main thread
if crossterm::event::poll(Duration::from_millis(100)).is_ok() {
if tx.send(crossterm::event::read().unwrap()).is_err() {
break
}
}
}
});
loop {
app.render()?;
app.handle(input.recv()?)?;
if app.exit {
app.stdout.queue(cursor::Hide)?.flush()?;
crossterm::terminal::disable_raw_mode()?;
break
}
}
Ok(())
}
fn handle (&mut self, event: crossterm::event::Event) -> Result<(), Box<dyn Error>> {
use crossterm::event::{Event, KeyCode, KeyModifiers};
if let Event::Key(event) = event {
match event.code {
KeyCode::Char('c') => {
if event.modifiers == KeyModifiers::CONTROL {
self.exit = true;
}
},
KeyCode::Down => {
self.selected_sample = (self.selected_sample + 1) % self.samples.len();
println!("{}", self.selected_sample);
},
KeyCode::Up => {
if self.selected_sample == 0 {
self.selected_sample = self.samples.len() - 1;
} else {
self.selected_sample = self.selected_sample - 1;
}
println!("{}", self.selected_sample);
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column = self.selected_column - 1;
}
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column = self.selected_column + 1;
}
},
_ => {
println!("{event:?}");
}
}
}
Ok(())
}
fn render (&mut self) -> Result<(), Box<dyn Error>> {
let (cols, rows) = terminal::size()?;
self.cols = cols;
self.rows = rows;
self.stdout.queue(terminal::Clear(terminal::ClearType::All))?;
self.stdout.queue(cursor::Hide)?;
self.render_toolbar()?;
self.render_table()?;
self.render_meters()?;
self.stdout.flush()?;
Ok(())
}
fn render_toolbar (&mut self) -> Result<(), Box<dyn Error>> {
self.stdout
.queue(cursor::MoveTo(1, 0))?
.queue(PrintStyledContent("[Arrows]".yellow().bold()))?
.queue(cursor::MoveTo(1, 1))?
.queue(PrintStyledContent("Navigate".yellow()))?
.queue(cursor::MoveTo(12, 0))?
.queue(PrintStyledContent("[Enter]".yellow().bold()))?
.queue(cursor::MoveTo(12, 1))?
.queue(PrintStyledContent("Play sample".yellow()))?
.queue(cursor::MoveTo(25, 0))?
.queue(PrintStyledContent("[Ins/Del]".yellow().bold()))?
.queue(cursor::MoveTo(25, 1))?
.queue(PrintStyledContent("Add/remove sample".yellow()))?;
Ok(())
}
fn render_table (&mut self) -> Result<(), Box<dyn Error>> {
self.stdout
.queue(cursor::MoveTo(0, 3))?
.queue(Print(
" Name Rate Trigger Route"))?;
for (i, sample) in self.samples.iter().enumerate() {
let row = 4 + i as u16;
for (j, (column, field)) in [
(0, format!(" {:7} ", sample.name)),
(9, format!(" {:.1}Hz ", sample.rate)),
(18, format!(" MIDI C10 36 ")),
(33, format!(" {:.1}dB -> Output ", sample.gain)),
].into_iter().enumerate() {
self.stdout.queue(cursor::MoveTo(column, row))?;
if self.selected_sample == i && self.selected_column == j {
self.stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
} else {
self.stdout.queue(PrintStyledContent(field.to_string().bold()))?;
}
}
}
Ok(())
}
fn render_meters (&mut self) -> Result<(), Box<dyn Error>> {
for (i, sample) in self.samples.iter().enumerate() {
let row = 4 + i as u16;
self.stdout
.queue(cursor::MoveTo(32, row))?
.queue(PrintStyledContent("".green()))?;
}
Ok(())
}
}
pub struct Sample {
@ -207,41 +62,3 @@ impl Sample {
}
}
struct Notifications;
impl NotificationHandler for Notifications {
fn thread_init (&self, _: &Client) {
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
}
fn freewheel (&mut self, _: &Client, is_enabled: bool) {
}
fn sample_rate (&mut self, _: &Client, _: Frames) -> Control {
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) {
}
fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) {
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
Control::Continue
}
fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) {
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}

49
src/sampler/handle.rs Normal file
View file

@ -0,0 +1,49 @@
use crate::prelude::*;
use super::Sampler;
pub fn handle (
state: &mut Sampler,
event: crossterm::event::Event
) -> Result<(), Box<dyn Error>> {
use crossterm::event::{Event, KeyCode, KeyModifiers};
if let Event::Key(event) = event {
match event.code {
KeyCode::Char('c') => {
if event.modifiers == KeyModifiers::CONTROL {
state.exit = true;
}
},
KeyCode::Down => {
state.selected_sample = (state.selected_sample + 1) % state.samples.len();
println!("{}", state.selected_sample);
},
KeyCode::Up => {
if state.selected_sample == 0 {
state.selected_sample = state.samples.len() - 1;
} else {
state.selected_sample = state.selected_sample - 1;
}
println!("{}", state.selected_sample);
},
KeyCode::Left => {
if state.selected_column == 0 {
state.selected_column = 6
} else {
state.selected_column = state.selected_column - 1;
}
},
KeyCode::Right => {
if state.selected_column == 6 {
state.selected_column = 0
} else {
state.selected_column = state.selected_column + 1;
}
},
_ => {
println!("{event:?}");
}
}
}
Ok(())
}

40
src/sampler/jack.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::prelude::*;
pub struct Notifications;
impl NotificationHandler for Notifications {
fn thread_init (&self, _: &Client) {
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
}
fn freewheel (&mut self, _: &Client, is_enabled: bool) {
}
fn sample_rate (&mut self, _: &Client, _: Frames) -> Control {
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) {
}
fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) {
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
Control::Continue
}
fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) {
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}

80
src/sampler/render.rs Normal file
View file

@ -0,0 +1,80 @@
use crate::prelude::*;
use super::Sampler;
pub fn render (
state: &mut Sampler,
stdout: &mut Stdout,
offset: (u16, u16),
) -> Result<(), Box<dyn Error>> {
render_toolbar(state, stdout, offset)?;
render_table(state, stdout, offset)?;
render_meters(state, stdout, offset)?;
stdout.flush()?;
Ok(())
}
fn render_toolbar (
state: &mut Sampler,
stdout: &mut Stdout,
offset: (u16, u16),
) -> Result<(), Box<dyn Error>> {
stdout
.queue(cursor::MoveTo(1, 0))?
.queue(PrintStyledContent("Arrows".yellow().bold()))?
.queue(cursor::MoveTo(1, 1))?
.queue(PrintStyledContent("Navigate".yellow()))?
.queue(cursor::MoveTo(11, 0))?
.queue(PrintStyledContent("Enter".yellow().bold()))?
.queue(cursor::MoveTo(11, 1))?
.queue(PrintStyledContent("Play sample".yellow()))?
.queue(cursor::MoveTo(24, 0))?
.queue(PrintStyledContent("Ins/Del".yellow().bold()))?
.queue(cursor::MoveTo(24, 1))?
.queue(PrintStyledContent("Add/remove sample".yellow()))?;
Ok(())
}
fn render_table (
state: &mut Sampler,
stdout: &mut Stdout,
offset: (u16, u16),
) -> Result<(), Box<dyn Error>> {
stdout
.queue(cursor::MoveTo(0, 3))?
.queue(Print(
" Name Rate Trigger Route"))?;
for (i, sample) in state.samples.iter().enumerate() {
let row = 4 + i as u16;
for (j, (column, field)) in [
(0, format!(" {:7} ", sample.name)),
(9, format!(" {:.1}Hz ", sample.rate)),
(18, format!(" MIDI C10 36 ")),
(33, format!(" {:.1}dB -> Output ", sample.gain)),
].into_iter().enumerate() {
stdout.queue(cursor::MoveTo(column, row))?;
if state.selected_sample == i && state.selected_column == j {
stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?;
} else {
stdout.queue(PrintStyledContent(field.to_string().bold()))?;
}
}
}
Ok(())
}
fn render_meters (
state: &mut Sampler,
stdout: &mut Stdout,
offset: (u16, u16),
) -> Result<(), Box<dyn Error>> {
for (i, sample) in state.samples.iter().enumerate() {
let row = 4 + i as u16;
stdout
.queue(cursor::MoveTo(32, row))?
.queue(PrintStyledContent("".green()))?;
}
Ok(())
}

39
src/sequencer.rs Normal file
View file

@ -0,0 +1,39 @@
mod handle;
mod render;
mod jack;
pub use self::handle::*;
pub use self::render::*;
pub use self::jack::*;
use crate::prelude::*;
pub struct Sequencer {
jack: Jack<Notifications>,
exit: bool,
stdout: Stdout,
cursor: (u16, u16),
duration: u16,
}
impl Sequencer {
pub fn new () -> Result<Self, Box<dyn Error>> {
let (client, status) = Client::new(
"bloop-sequencer",
ClientOptions::NO_START_SERVER
)?;
let jack = client.activate_async(
Notifications,
ClosureProcessHandler::new(Box::new(
move |_: &Client, _: &ProcessScope| -> Control {
Control::Continue
}) as Box<dyn FnMut(&Client, &ProcessScope)->Control + Send>
)
)?;
Ok(Self {
exit: false,
stdout: std::io::stdout(),
cursor: (0, 0),
duration: 0,
jack,
})
}
}

58
src/sequencer/handle.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::prelude::*;
use super::Sequencer;
fn handle (
state: &mut Sequencer,
event: crossterm::event::Event
) -> Result<(), Box<dyn Error>> {
use crossterm::event::{Event, KeyCode, KeyModifiers};
if let Event::Key(event) = event {
match event.code {
KeyCode::Char('c') => {
if event.modifiers == KeyModifiers::CONTROL {
state.exit = true;
}
},
KeyCode::Char('[') => {
if state.duration > 0 {
state.duration = state.duration - 1
}
},
KeyCode::Char(']') => {
state.duration = state.duration + 1
},
KeyCode::Down => {
state.cursor.1 = if state.cursor.1 >= 3 {
0
} else {
state.cursor.1 + 1
}
},
KeyCode::Up => {
state.cursor.1 = if state.cursor.1 == 0 {
3
} else {
state.cursor.1 - 1
}
},
KeyCode::Left => {
state.cursor.0 = if state.cursor.0 == 0 {
63
} else {
state.cursor.0 - 1
}
},
KeyCode::Right => {
state.cursor.0 = if state.cursor.0 == 63 {
0
} else {
state.cursor.0 + 1
}
},
_ => {
println!("{event:?}");
}
}
}
Ok(())
}

40
src/sequencer/jack.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::prelude::*;
pub struct Notifications;
impl NotificationHandler for Notifications {
fn thread_init (&self, _: &Client) {
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
}
fn freewheel (&mut self, _: &Client, is_enabled: bool) {
}
fn sample_rate (&mut self, _: &Client, _: Frames) -> Control {
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) {
}
fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) {
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
Control::Continue
}
fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) {
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}

82
src/sequencer/render.rs Normal file
View file

@ -0,0 +1,82 @@
use crate::prelude::*;
use super::Sequencer;
pub fn render (
state: &mut Sequencer,
stdout: &mut Stdout,
offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
render_toolbar(state, stdout, offset)?;
render_grid(state, stdout, offset)?;
render_events(state, stdout, offset)?;
Ok(())
}
fn render_toolbar (
state: &mut Sequencer,
stdout: &mut Stdout,
offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
stdout
.queue(MoveTo(1, 0))?
.queue(PrintStyledContent("Arrows".yellow().bold()))?
.queue(MoveTo(1, 1))?
.queue(PrintStyledContent("Navigate".yellow()))?
.queue(MoveTo(12, 0))?
.queue(PrintStyledContent("+/-".yellow().bold()))?
.queue(MoveTo(12, 1))?
.queue(PrintStyledContent("Zoom".yellow()))?
.queue(MoveTo(20, 0))?
.queue(PrintStyledContent("a/d".yellow().bold()))?
.queue(MoveTo(20, 1))?
.queue(PrintStyledContent("Add/delete".yellow()))?
.queue(MoveTo(33, 0))?
.queue(PrintStyledContent("[/]".yellow().bold()))?
.queue(MoveTo(33, 1))?
.queue(PrintStyledContent("Duration".yellow()))?
.queue(MoveTo(45, 0))?
.queue(PrintStyledContent("CapsLock".yellow().bold()))?
.queue(MoveTo(45, 1))?
.queue(PrintStyledContent("Auto advance".yellow()))?;
Ok(())
}
fn render_grid (
state: &mut Sequencer,
stdout: &mut Stdout,
offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
let bg = "┊···············┊···············┊···············┊···············";
let cursor: String = if state.duration == 0 {
"X".into()
} else {
std::iter::repeat("·")
.take(state.duration as usize)
.collect()
};
stdout
.queue(MoveTo(1, 3))?.queue(Print("1.1"))?
.queue(MoveTo(17, 3))?.queue(Print("1.2"))?
.queue(MoveTo(33, 3))?.queue(Print("1.3"))?
.queue(MoveTo(49, 3))?.queue(Print("1.4"))?
.queue(MoveTo(1, 4))?.queue(PrintStyledContent(bg.grey()))?
.queue(MoveTo(1, 5))?.queue(PrintStyledContent(bg.grey()))?
.queue(MoveTo(1, 6))?.queue(PrintStyledContent(bg.grey()))?
.queue(MoveTo(1, 7))?.queue(PrintStyledContent(bg.grey()))?
.queue(MoveTo(1 + state.cursor.0, 4 + state.cursor.1))?
.queue(PrintStyledContent(cursor.reverse()))?;
Ok(())
}
fn render_events (
state: &mut Sequencer,
stdout: &mut Stdout,
offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
Ok(())
}

View file

@ -1,58 +1,22 @@
use std::error::Error;
use std::io::Write;
mod handle;
mod jack;
mod render;
pub use self::handle::*;
pub use self::jack::*;
pub use self::render::*;
use crate::prelude::*;
pub struct Transport {
cols: u16,
rows: u16,
}
pub struct Transport;
impl Transport {
pub fn run_tui () -> Result<(), Box<dyn Error>> {
let mut app = Self { cols: 0, rows: 0 };
let sleep = std::time::Duration::from_millis(16);
let mut stdout = std::io::stdout();
loop {
app.render(&mut stdout)?;
std::thread::sleep(sleep);
}
}
fn render (&mut self, stdout: &mut std::io::Stdout) -> Result<(), Box<dyn Error>> {
use crossterm::{*, style::{*, Stylize}};
let (cols, rows) = terminal::size()?;
if cols != self.cols || rows != self.rows {
self.cols = cols;
self.rows = rows;
stdout
.queue(terminal::Clear(terminal::ClearType::All))?
.queue(cursor::Hide)?
.queue(cursor::MoveTo(1, 0))?
.queue(PrintStyledContent("[Home]".yellow().bold()))?
.queue(cursor::MoveTo(1, 1))?
.queue(PrintStyledContent("⏹ Stop and rewind".yellow().bold()))?
.queue(cursor::MoveTo(20, 0))?
.queue(PrintStyledContent("[Enter]".yellow().bold()))?
.queue(cursor::MoveTo(20, 1))?
.queue(PrintStyledContent("⏮ Play from start".yellow().bold()))?
.queue(cursor::MoveTo(40, 0))?
.queue(PrintStyledContent("[Space]".yellow().bold()))?
.queue(cursor::MoveTo(40, 1))?
.queue(PrintStyledContent("⯈ Play from cursor".yellow().bold()))?
.queue(cursor::MoveTo(1, 3))?.queue(Print("Rate: "))?
.queue(cursor::MoveTo(7, 3))?.queue(PrintStyledContent("48000Hz".white().bold()))?
.queue(cursor::MoveTo(20, 3))?.queue(Print("BPM: "))?
.queue(cursor::MoveTo(25, 3))?.queue(PrintStyledContent("120.34".white().bold()))?
.queue(cursor::MoveTo(35, 3))?.queue(Print("Signature: "))?
.queue(cursor::MoveTo(46, 3))?.queue(PrintStyledContent("4 / 4".white().bold()))?
.queue(cursor::MoveTo(1, 4))?.queue(Print("Time: "))?
.queue(cursor::MoveTo(7, 4))?.queue(PrintStyledContent("1m 23.456s".white().bold()))?
.queue(cursor::MoveTo(20, 4))?.queue(Print("Beat: "))?
.queue(cursor::MoveTo(26, 4))?.queue(PrintStyledContent("30x 3/4".white().bold()))?
.flush()?;
}
Ok(())
pub fn new () -> Result<Self, Box<dyn Error>> {
Ok(Self)
}
}
const ACTIONS: [(&'static str, &'static str);3] = [
("Tab/Shift-Tab, Arrows", "Navigate"),
//("Home", "⏹ Stop and rewind"),
("Space", "⏮ Play from cursor/pause"),
("Shift-Space", "⯈ Play from start/rewind"),
];

1
src/transport/handle.rs Normal file
View file

@ -0,0 +1 @@
use crate::prelude::*;

40
src/transport/jack.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::prelude::*;
struct Notifications;
impl NotificationHandler for Notifications {
fn thread_init (&self, _: &Client) {
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
}
fn freewheel (&mut self, _: &Client, is_enabled: bool) {
}
fn sample_rate (&mut self, _: &Client, _: Frames) -> Control {
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) {
}
fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) {
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
Control::Continue
}
fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) {
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}

40
src/transport/render.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::prelude::*;
use super::{Transport, ACTIONS};
pub fn render (
state: &mut Transport,
stdout: &mut Stdout,
mut offset: (u16, u16)
) -> Result<(), Box<dyn Error>> {
use crossterm::{*, cursor::{Hide, MoveTo}, style::{*, Stylize}};
let (w, h) = crate::render::render_toolbar_vertical(stdout, offset, &ACTIONS)?;
offset.0 = offset.0 + w + 2;
let move_to = |col, row| MoveTo(offset.0 + col, offset.1 + row);
stdout.queue(move_to( 1, 0))?.queue(
Print("Project: ")
)?.queue(move_to(10, 0))?.queue(
PrintStyledContent("The Quick Brown Fox - Jumping Over Lazy Dogs".white().bold())
)?.queue(move_to( 1, 2))?.queue(
Print("Rate: ")
)?.queue(move_to( 7, 2))?.queue(
PrintStyledContent("48000Hz".white().bold())
)?.queue(move_to(20, 2))?.queue(
Print("BPM: ")
)?.queue(move_to(25, 2))?.queue(
PrintStyledContent("120.34".white().bold())
)?.queue(move_to(35, 2))?.queue(
Print("Signature: ")
)?.queue(move_to(46, 2))?.queue(
PrintStyledContent("4 / 4".white().bold())
)?.queue(move_to( 1, 4))?.queue(
Print("Time: ")
)?.queue(move_to( 7, 4))?.queue(
PrintStyledContent("1m 23.456s".white().bold())
)?.queue(move_to(20, 4))?.queue(
Print("Beat: ")
)?.queue(move_to(26, 4))?.queue(
PrintStyledContent("30x 3/4".white().bold())
)?;
Ok(())
}