open ports; start work on sampler

This commit is contained in:
🪞👃🪞 2024-05-24 19:41:07 +03:00
parent 11a9f3ba50
commit f9218e887a
5 changed files with 501 additions and 96 deletions

View file

@ -1,5 +1,6 @@
[package] [package]
name = "bloop" name = "bloop"
edition = "2021"
[dependencies] [dependencies]
jack = "0.10" jack = "0.10"

View file

@ -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,
} }

View file

@ -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
View 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
View 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
}
}