mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
open ports; start work on sampler
This commit is contained in:
parent
11a9f3ba50
commit
f9218e887a
5 changed files with 501 additions and 96 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bloop"
|
name = "bloop"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jack = "0.10"
|
jack = "0.10"
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ use clap::{Parser, Subcommand};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
//pub mod sequence;
|
//pub mod sequence;
|
||||||
|
pub mod prelude;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
pub mod mixer;
|
pub mod mixer;
|
||||||
pub mod looper;
|
pub mod looper;
|
||||||
|
pub mod sampler;
|
||||||
|
|
||||||
fn main () -> Result<(), Box<dyn Error>> {
|
fn main () -> Result<(), Box<dyn Error>> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
@ -20,6 +22,8 @@ fn main () -> Result<(), Box<dyn Error>> {
|
||||||
crate::mixer::Mixer::run_tui(),
|
crate::mixer::Mixer::run_tui(),
|
||||||
Command::Looper =>
|
Command::Looper =>
|
||||||
crate::looper::Looper::run_tui(),
|
crate::looper::Looper::run_tui(),
|
||||||
|
Command::Sampler =>
|
||||||
|
crate::sampler::Sampler::run_tui(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,4 +42,6 @@ pub enum Command {
|
||||||
Mixer,
|
Mixer,
|
||||||
/// Control the looper
|
/// Control the looper
|
||||||
Looper,
|
Looper,
|
||||||
|
/// Control the sampler
|
||||||
|
Sampler,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
309
src/mixer.rs
309
src/mixer.rs
|
|
@ -1,69 +1,59 @@
|
||||||
use std::error::Error;
|
use crate::prelude::*;
|
||||||
use std::io::{stdout, Write};
|
|
||||||
use std::thread::spawn;
|
// TODO:
|
||||||
use std::time::Duration;
|
// - Meters: propagate clipping:
|
||||||
use std::sync::{
|
// - If one stage clips, all stages after it are marked red
|
||||||
Arc,
|
// - If one track clips, all tracks that feed from it are marked red?
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
mpsc::{channel, Sender, Receiver}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Mixer {
|
pub struct Mixer {
|
||||||
exit: bool,
|
jack: Jack<Notifications>,
|
||||||
cols: u16,
|
exit: bool,
|
||||||
rows: u16,
|
stdout: Stdout,
|
||||||
tracks: Vec<Track>,
|
cols: u16,
|
||||||
selected_track: usize,
|
rows: u16,
|
||||||
|
tracks: Vec<Track>,
|
||||||
|
selected_track: usize,
|
||||||
selected_column: usize,
|
selected_column: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Track {
|
|
||||||
name: String,
|
|
||||||
gain: f64,
|
|
||||||
level: f64,
|
|
||||||
pan: f64,
|
|
||||||
route: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mixer {
|
impl Mixer {
|
||||||
|
|
||||||
pub fn new () -> Self {
|
pub fn new () -> Result<Self, Box<dyn Error>> {
|
||||||
Self {
|
let (client, status) = Client::new(
|
||||||
exit: false,
|
"bloop-mixer",
|
||||||
cols: 0,
|
ClientOptions::NO_START_SERVER
|
||||||
rows: 0,
|
)?;
|
||||||
|
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(),
|
||||||
|
cols: 0,
|
||||||
|
rows: 0,
|
||||||
selected_column: 0,
|
selected_column: 0,
|
||||||
selected_track: 1,
|
selected_track: 1,
|
||||||
tracks: vec! [
|
tracks: vec![
|
||||||
Track {
|
Track::new(&jack.as_client(), 1, "Kick")?,
|
||||||
name: "Track 1".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 1".into()
|
Track::new(&jack.as_client(), 1, "Snare")?,
|
||||||
},
|
Track::new(&jack.as_client(), 2, "Hihats")?,
|
||||||
Track {
|
Track::new(&jack.as_client(), 2, "Sample")?,
|
||||||
name: "Track 2".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 2".into()
|
Track::new(&jack.as_client(), 2, "Bus 1")?,
|
||||||
},
|
Track::new(&jack.as_client(), 2, "Bus 2")?,
|
||||||
Track {
|
Track::new(&jack.as_client(), 2, "Mix")?,
|
||||||
name: "Track 3".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 1".into()
|
],
|
||||||
},
|
jack,
|
||||||
Track {
|
})
|
||||||
name: "Track 4".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 2".into()
|
|
||||||
},
|
|
||||||
Track {
|
|
||||||
name: "Bus 1".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Mix".into()
|
|
||||||
},
|
|
||||||
Track {
|
|
||||||
name: "Bus 2".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Mix".into()
|
|
||||||
},
|
|
||||||
Track {
|
|
||||||
name: "Mix".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Output".into()
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_tui () -> Result<(), Box<dyn Error>> {
|
pub fn run_tui () -> Result<(), Box<dyn Error>> {
|
||||||
let mut app = Self::new();
|
let mut app = Self::new()?;
|
||||||
let sleep = std::time::Duration::from_millis(16);
|
let sleep = std::time::Duration::from_millis(16);
|
||||||
let mut stdout = std::io::stdout();
|
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
let (tx, input) = channel::<crossterm::event::Event>();
|
let (tx, input) = channel::<crossterm::event::Event>();
|
||||||
let exited = Arc::new(AtomicBool::new(false));
|
let exited = Arc::new(AtomicBool::new(false));
|
||||||
|
|
@ -84,9 +74,10 @@ impl Mixer {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loop {
|
loop {
|
||||||
app.render(&mut stdout)?;
|
app.render()?;
|
||||||
app.handle(input.recv()?)?;
|
app.handle(input.recv()?)?;
|
||||||
if app.exit {
|
if app.exit {
|
||||||
|
app.stdout.queue(cursor::Hide)?.flush()?;
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +86,6 @@ impl Mixer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle (&mut self, event: crossterm::event::Event) -> Result<(), Box<dyn Error>> {
|
fn handle (&mut self, event: crossterm::event::Event) -> Result<(), Box<dyn Error>> {
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||||
if let Event::Key(event) = event {
|
if let Event::Key(event) = event {
|
||||||
match event.code {
|
match event.code {
|
||||||
|
|
@ -107,7 +97,7 @@ impl Mixer {
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
self.selected_track = (self.selected_track + 1) % self.tracks.len();
|
self.selected_track = (self.selected_track + 1) % self.tracks.len();
|
||||||
println!("{}", self.selected_track);
|
println!("{}", self.selected_track);
|
||||||
}
|
},
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
if self.selected_track == 0 {
|
if self.selected_track == 0 {
|
||||||
self.selected_track = self.tracks.len() - 1;
|
self.selected_track = self.tracks.len() - 1;
|
||||||
|
|
@ -115,7 +105,21 @@ impl Mixer {
|
||||||
self.selected_track = self.selected_track - 1;
|
self.selected_track = self.selected_track - 1;
|
||||||
}
|
}
|
||||||
println!("{}", self.selected_track);
|
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:?}");
|
println!("{event:?}");
|
||||||
}
|
}
|
||||||
|
|
@ -124,54 +128,167 @@ impl Mixer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render (&mut self, stdout: &mut std::io::Stdout) -> Result<(), Box<dyn Error>> {
|
fn render (&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
use crossterm::{*, style::{*, Stylize}};
|
|
||||||
let (cols, rows) = terminal::size()?;
|
let (cols, rows) = terminal::size()?;
|
||||||
if true || cols != self.cols || rows != self.rows { // TODO perf
|
self.cols = cols;
|
||||||
self.cols = cols;
|
self.rows = rows;
|
||||||
self.rows = rows;
|
self.stdout.queue(terminal::Clear(terminal::ClearType::All))?;
|
||||||
stdout
|
self.stdout.queue(cursor::Hide)?;
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))?
|
self.render_toolbar()?;
|
||||||
.queue(cursor::Hide)?
|
self.render_table()?;
|
||||||
|
self.render_meters()?;
|
||||||
|
self.stdout.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
.queue(cursor::MoveTo(1, 0))?
|
fn render_toolbar (&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
.queue(PrintStyledContent("[Arrows]".yellow().bold()))?
|
self.stdout
|
||||||
.queue(cursor::MoveTo(1, 1))?
|
.queue(cursor::MoveTo(1, 0))?
|
||||||
.queue(PrintStyledContent("Navigate".yellow()))?
|
.queue(PrintStyledContent("[Arrows]".yellow().bold()))?
|
||||||
|
.queue(cursor::MoveTo(1, 1))?
|
||||||
|
.queue(PrintStyledContent("Navigate".yellow()))?
|
||||||
|
|
||||||
.queue(cursor::MoveTo(11, 0))?
|
.queue(cursor::MoveTo(11, 0))?
|
||||||
.queue(PrintStyledContent("[+/-]".yellow().bold()))?
|
.queue(PrintStyledContent("[+/-]".yellow().bold()))?
|
||||||
.queue(cursor::MoveTo(11, 1))?
|
.queue(cursor::MoveTo(11, 1))?
|
||||||
.queue(PrintStyledContent("Adjust value".yellow()))?
|
.queue(PrintStyledContent("Adjust value".yellow()))?
|
||||||
|
|
||||||
.queue(cursor::MoveTo(25, 0))?
|
.queue(cursor::MoveTo(25, 0))?
|
||||||
.queue(PrintStyledContent("[Ins/Del]".yellow().bold()))?
|
.queue(PrintStyledContent("[Ins/Del]".yellow().bold()))?
|
||||||
.queue(cursor::MoveTo(25, 1))?
|
.queue(cursor::MoveTo(25, 1))?
|
||||||
.queue(PrintStyledContent("Add/remove track".yellow()))?
|
.queue(PrintStyledContent("Add/remove track".yellow()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
.queue(cursor::MoveTo(0, 3))?.queue(Print(
|
fn render_table (&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
" Name Gain Pre Level Pan Post Route"))?;
|
self.stdout
|
||||||
|
.queue(cursor::MoveTo(0, 3))?
|
||||||
for (i, track) in self.tracks.iter().enumerate() {
|
.queue(Print(
|
||||||
let row = 4 + i as u16;
|
" Name Gain FX1 Pan Level FX2 Route"))?;
|
||||||
let mut content = format!(
|
for (i, track) in self.tracks.iter().enumerate() {
|
||||||
" {:7} █ {:.1}dB █ [ ] █ {:.1}dB C █ [ ] {:7} ",
|
let row = 4 + i as u16;
|
||||||
track.name,
|
for (j, (column, field)) in [
|
||||||
track.gain,
|
(0, format!(" {:7} ", track.name)),
|
||||||
track.level,
|
(12, format!(" {:.1}dB ", track.gain)),
|
||||||
track.route,
|
(22, format!(" [ ] ")),
|
||||||
).bold();
|
(30, format!(" C ")),
|
||||||
if i == self.selected_track {
|
(35, format!(" {:.1}dB ", track.level)),
|
||||||
content = content.reverse();
|
(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()))?;
|
||||||
}
|
}
|
||||||
stdout
|
|
||||||
.queue(cursor::MoveTo(0, row))?
|
|
||||||
.queue(PrintStyledContent(content))?;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
stdout
|
fn render_meters (&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
.flush()?;
|
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(())
|
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 {
|
||||||
|
pub fn new (jack: &Client, channels: u8, name: &str) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let mut input_ports = vec![];
|
||||||
|
let mut insert_ports = vec![];
|
||||||
|
let mut return_ports = vec![];
|
||||||
|
let mut output_ports = vec![];
|
||||||
|
for channel in 1..=channels {
|
||||||
|
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
|
||||||
|
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
|
||||||
|
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
|
||||||
|
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
|
||||||
|
jack.connect_ports(&insert_port, &return_port)?;
|
||||||
|
insert_ports.push(insert_port);
|
||||||
|
return_ports.push(return_port);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
name: name.into(),
|
||||||
|
channels,
|
||||||
|
input_ports,
|
||||||
|
pre_gain_meter: 0.0,
|
||||||
|
gain: 0.0,
|
||||||
|
post_gain_meter: 0.0,
|
||||||
|
insert_ports,
|
||||||
|
return_ports,
|
||||||
|
post_insert_meter: 0.0,
|
||||||
|
level: 0.0,
|
||||||
|
pan: 0.0,
|
||||||
|
post_fader_meter: 0.0,
|
||||||
|
route: "---".into(),
|
||||||
|
output_ports,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
34
src/prelude.rs
Normal file
34
src/prelude.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
pub use std::error::Error;
|
||||||
|
pub use std::io::{stdout, Stdout, Write};
|
||||||
|
pub use std::thread::spawn;
|
||||||
|
pub use std::time::Duration;
|
||||||
|
pub use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc::{channel, Sender, Receiver}
|
||||||
|
};
|
||||||
|
pub use crossterm::{
|
||||||
|
QueueableCommand,
|
||||||
|
cursor,
|
||||||
|
terminal,
|
||||||
|
style::*,
|
||||||
|
};
|
||||||
|
pub use jack::{
|
||||||
|
AsyncClient,
|
||||||
|
AudioIn,
|
||||||
|
AudioOut,
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
ClientStatus,
|
||||||
|
ClosureProcessHandler,
|
||||||
|
Control,
|
||||||
|
Frames,
|
||||||
|
NotificationHandler,
|
||||||
|
Port,
|
||||||
|
PortId,
|
||||||
|
ProcessScope,
|
||||||
|
};
|
||||||
|
pub type Jack<N> = AsyncClient<
|
||||||
|
N,
|
||||||
|
ClosureProcessHandler<Box<dyn FnMut(&Client, &ProcessScope)-> Control + Send>>
|
||||||
|
>;
|
||||||
247
src/sampler.rs
Normal file
247
src/sampler.rs
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
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",
|
||||||
|
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(),
|
||||||
|
cols: 0,
|
||||||
|
rows: 0,
|
||||||
|
selected_sample: 0,
|
||||||
|
selected_column: 0,
|
||||||
|
samples: vec![
|
||||||
|
Sample::new("Kick")?,
|
||||||
|
Sample::new("Snare")?,
|
||||||
|
],
|
||||||
|
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 {
|
||||||
|
name: String,
|
||||||
|
rate: u32,
|
||||||
|
gain: f64,
|
||||||
|
channels: u8,
|
||||||
|
data: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sample {
|
||||||
|
|
||||||
|
pub fn new (name: &str) -> Result<Self, Box<dyn Error>> {
|
||||||
|
Ok(Self {
|
||||||
|
name: name.into(),
|
||||||
|
rate: 44100,
|
||||||
|
channels: 1,
|
||||||
|
gain: 0.0,
|
||||||
|
data: vec![vec![]]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue