mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
wip: running interface in separate or combined mode
also disassociating render functions from state structs
This commit is contained in:
parent
f9218e887a
commit
d6bf840a1f
31 changed files with 905 additions and 532 deletions
22
src/cli.rs
Normal file
22
src/cli.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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
2
src/looper/handle.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
use crate::prelude::*;
|
||||
use super::Looper;
|
||||
0
src/looper/jack.rs
Normal file
0
src/looper/jack.rs
Normal file
20
src/looper/render.rs
Normal file
20
src/looper/render.rs
Normal 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(())
|
||||
}
|
||||
157
src/main.rs
157
src/main.rs
|
|
@ -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(())
|
||||
//}
|
||||
|
|
|
|||
240
src/mixer.rs
240
src/mixer.rs
|
|
@ -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
48
src/mixer/handle.rs
Normal 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
40
src/mixer/jack.rs
Normal 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
66
src/mixer/render.rs
Normal 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(())
|
||||
}
|
||||
|
||||
|
|
@ -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
41
src/render.rs
Normal 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(())
|
||||
}
|
||||
195
src/sampler.rs
195
src/sampler.rs
|
|
@ -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
49
src/sampler/handle.rs
Normal 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
40
src/sampler/jack.rs
Normal 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
80
src/sampler/render.rs
Normal 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
39
src/sequencer.rs
Normal 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
58
src/sequencer/handle.rs
Normal 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
40
src/sequencer/jack.rs
Normal 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
82
src/sequencer/render.rs
Normal 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(())
|
||||
}
|
||||
|
||||
|
|
@ -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
1
src/transport/handle.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
use crate::prelude::*;
|
||||
40
src/transport/jack.rs
Normal file
40
src/transport/jack.rs
Normal 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
40
src/transport/render.rs
Normal 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(())
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue