Implemented osc as client
This commit is contained in:
parent
d5665b841c
commit
1bbb020566
274
audio_engine/Cargo.lock
generated
274
audio_engine/Cargo.lock
generated
@ -17,20 +17,75 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "audio_engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"clap",
|
||||
"dirs",
|
||||
"futures",
|
||||
"hound",
|
||||
"jack",
|
||||
"kanal",
|
||||
"log",
|
||||
"rosc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_logger",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"wmidi",
|
||||
]
|
||||
|
||||
@ -67,6 +122,12 @@ version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
@ -79,6 +140,52 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.2.0"
|
||||
@ -119,12 +226,95 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
@ -142,12 +332,24 @@ version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@ -245,6 +447,12 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.8"
|
||||
@ -265,6 +473,16 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@ -289,6 +507,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@ -324,6 +548,12 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@ -374,6 +604,16 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rosc"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e63d9e6b0d090be1485cf159b1e04c3973d2d3e1614963544ea2ff47a4a981"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@ -445,6 +685,15 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
@ -461,6 +710,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
@ -554,12 +809,31 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
||||
@ -4,14 +4,19 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
clap = { version = "4", features = ["derive", "env", "string"] }
|
||||
dirs = "5"
|
||||
futures = "0.3"
|
||||
hound = "3.5"
|
||||
jack = "0.13"
|
||||
kanal = "0.1"
|
||||
log = "0.4"
|
||||
rosc = "0.10"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
simple_logger = "5"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
wmidi = "4"
|
||||
33
audio_engine/src/args.rs
Normal file
33
audio_engine/src/args.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
use crate::*;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "audio_engine")]
|
||||
#[command(about = "FCB1010 Looper Pedal Audio Engine")]
|
||||
pub struct Args {
|
||||
/// Path to Unix socket or named pipe name
|
||||
#[arg(short = 's', long = "socket", env = "SOCKET", default_value = "/tmp/fcb_looper.sock")]
|
||||
pub socket: String,
|
||||
|
||||
/// Path to configuration file
|
||||
#[arg(short = 'c', long = "config-file", env = "CONFIG_FILE", default_value = default_config_path())]
|
||||
pub config_file: PathBuf,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn new() -> Result<Self> {
|
||||
let res = Self::try_parse();
|
||||
if let Err(res) = &res {
|
||||
log::error!("{res}");
|
||||
}
|
||||
res.map_err(|_| LooperError::ArgsParse(std::panic::Location::caller()))
|
||||
}
|
||||
}
|
||||
|
||||
fn default_config_path() -> String {
|
||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
path.push(".fcb_looper");
|
||||
path.push("state.json");
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
@ -77,37 +77,42 @@ impl<const ROWS: usize> Column<ROWS> {
|
||||
self.tracks[row].set_consolidated_buffer(buffer)
|
||||
}
|
||||
|
||||
pub fn handle_xrun<H>(
|
||||
pub fn handle_xrun<PRC, OC>(
|
||||
&mut self,
|
||||
timing: &BufferTiming,
|
||||
chunk_factory: &mut impl ChunkFactory,
|
||||
post_record_handler: H,
|
||||
post_record_controller: PRC,
|
||||
osc_controller: OC,
|
||||
) -> Result<()>
|
||||
where
|
||||
H: Fn(usize, Arc<AudioChunk>, usize) -> Result<()>,
|
||||
PRC: Fn(usize, Arc<AudioChunk>, usize) -> Result<()>,
|
||||
OC: Fn(usize, TrackState) -> Result<()>,
|
||||
{
|
||||
for (row, track) in self.tracks.iter_mut().enumerate() {
|
||||
track.handle_xrun(
|
||||
timing.beat_in_missed,
|
||||
timing.missed_frames,
|
||||
chunk_factory,
|
||||
|chunk, sync_offset| post_record_handler(row, chunk, sync_offset),
|
||||
|chunk, sync_offset| post_record_controller(row, chunk, sync_offset),
|
||||
|state| osc_controller(row, state),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process<H>(
|
||||
pub fn process<PRC, OC>(
|
||||
&mut self,
|
||||
timing: &BufferTiming,
|
||||
input_buffer: &[f32],
|
||||
output_buffer: &mut [f32],
|
||||
scratch_pad: &mut [f32],
|
||||
chunk_factory: &mut impl ChunkFactory,
|
||||
post_record_handler: H,
|
||||
post_record_controller: PRC,
|
||||
osc_controller: OC,
|
||||
) -> Result<()>
|
||||
where
|
||||
H: Fn(usize, Arc<AudioChunk>, usize) -> Result<()>,
|
||||
PRC: Fn(usize, Arc<AudioChunk>, usize) -> Result<()>,
|
||||
OC: Fn(usize, TrackState) -> Result<()>,
|
||||
{
|
||||
let len = self.len();
|
||||
if self.idle() {
|
||||
@ -127,7 +132,8 @@ impl<const ROWS: usize> Column<ROWS> {
|
||||
input_buffer,
|
||||
scratch_pad,
|
||||
chunk_factory,
|
||||
|chunk, sync_offset| post_record_handler(row, chunk, sync_offset),
|
||||
|chunk, sync_offset| post_record_controller(row, chunk, sync_offset),
|
||||
|state| osc_controller(row, state),
|
||||
)?;
|
||||
for (output_val, scratch_pad_val) in output_buffer.iter_mut().zip(scratch_pad.iter()) {
|
||||
*output_val += *scratch_pad_val;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LooperError {
|
||||
#[error("Failed to parse command line arguments")]
|
||||
ArgsParse(&'static std::panic::Location<'static>),
|
||||
|
||||
#[error("Failed to allocate new audio chunk")]
|
||||
ChunkAllocation(&'static std::panic::Location<'static>),
|
||||
|
||||
@ -9,6 +12,9 @@ pub enum LooperError {
|
||||
#[error("Index out of bounds")]
|
||||
OutOfBounds(&'static std::panic::Location<'static>),
|
||||
|
||||
#[error("Failed to send OSC message")]
|
||||
Osc(&'static std::panic::Location<'static>),
|
||||
|
||||
#[error("Failed to load state")]
|
||||
StateLoad(&'static std::panic::Location<'static>),
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
mod allocator;
|
||||
mod args;
|
||||
mod audio_chunk;
|
||||
mod audio_data;
|
||||
mod beep;
|
||||
@ -9,6 +10,7 @@ mod looper_error;
|
||||
mod metronome;
|
||||
mod midi;
|
||||
mod notification_handler;
|
||||
mod osc;
|
||||
mod persistence_manager;
|
||||
mod post_record_handler;
|
||||
mod process_handler;
|
||||
@ -19,6 +21,7 @@ mod track_matrix;
|
||||
use std::sync::Arc;
|
||||
|
||||
use allocator::Allocator;
|
||||
use args::Args;
|
||||
use audio_chunk::AudioChunk;
|
||||
use audio_data::AudioData;
|
||||
use beep::generate_beep;
|
||||
@ -31,12 +34,15 @@ use metronome::BufferTiming;
|
||||
use metronome::Metronome;
|
||||
use notification_handler::JackNotification;
|
||||
use notification_handler::NotificationHandler;
|
||||
use osc::OscController;
|
||||
use osc::Osc;
|
||||
use persistence_manager::PersistenceManager;
|
||||
use post_record_handler::PostRecordController;
|
||||
use post_record_handler::PostRecordHandler;
|
||||
use process_handler::ProcessHandler;
|
||||
use state::State;
|
||||
use track::Track;
|
||||
use track::TrackState;
|
||||
use track_matrix::TrackMatrix;
|
||||
|
||||
pub struct JackPorts {
|
||||
@ -48,10 +54,13 @@ pub struct JackPorts {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::new().expect("Could not parse arguments");
|
||||
simple_logger::SimpleLogger::new()
|
||||
.init()
|
||||
.expect("Could not initialize logger");
|
||||
|
||||
let (mut osc, osc_controller) = Osc::new(&args.socket).await.expect("Could not create OSC server");
|
||||
|
||||
let (jack_client, ports) = setup_jack();
|
||||
|
||||
let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3);
|
||||
@ -63,7 +72,7 @@ async fn main() {
|
||||
let mut notification_channel = notification_handler.subscribe();
|
||||
|
||||
let (mut persistence_manager, state_watch) =
|
||||
PersistenceManager::new(notification_handler.subscribe());
|
||||
PersistenceManager::new(&args, notification_handler.subscribe());
|
||||
|
||||
// Load state values for metronome configuration
|
||||
let initial_state = state_watch.borrow().clone();
|
||||
@ -78,6 +87,7 @@ async fn main() {
|
||||
allocator,
|
||||
beep_samples,
|
||||
&initial_state,
|
||||
osc_controller,
|
||||
post_record_controller,
|
||||
)
|
||||
.expect("Could not create process handler");
|
||||
@ -95,6 +105,12 @@ async fn main() {
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = osc.run() => {
|
||||
if let Err(e) = result {
|
||||
log::error!("OSC task failed: {}", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
notification = notification_channel.recv() => {
|
||||
if let Ok(JackNotification::Shutdown {reason, status}) = notification {
|
||||
log::error!("Jack shutdown: {reason} {status:?}");
|
||||
|
||||
@ -12,7 +12,7 @@ pub fn process_events<F: ChunkFactory>(
|
||||
let mut event_count = 0;
|
||||
|
||||
// Collect events from the MIDI input iterator
|
||||
let midi_input = process_handler.ports.midi_in.iter(ps);
|
||||
let midi_input = process_handler.ports().midi_in.iter(ps);
|
||||
for midi_event in midi_input {
|
||||
if event_count < MAX_EVENTS && midi_event.bytes.len() >= 3 {
|
||||
raw_events[event_count][0] = midi_event.bytes[0];
|
||||
|
||||
175
audio_engine/src/osc.rs
Normal file
175
audio_engine/src/osc.rs
Normal file
@ -0,0 +1,175 @@
|
||||
use crate::*;
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
use futures::SinkExt;
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
#[cfg(windows)]
|
||||
use tokio::net::windows::named_pipe::ClientOptions;
|
||||
|
||||
#[cfg(unix)]
|
||||
type PlatformStream = UnixStream;
|
||||
#[cfg(windows)]
|
||||
type PlatformStream = tokio::net::windows::named_pipe::NamedPipeClient;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum OscMessage {
|
||||
TrackStateChanged {
|
||||
column: usize,
|
||||
row: usize,
|
||||
state: TrackState,
|
||||
},
|
||||
SelectedColumnChanged {
|
||||
column: usize,
|
||||
},
|
||||
SelectedRowChanged {
|
||||
row: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OscController {
|
||||
sender: kanal::Sender<OscMessage>,
|
||||
}
|
||||
|
||||
impl OscController {
|
||||
pub fn track_state_changed(
|
||||
&self,
|
||||
column: usize,
|
||||
row: usize,
|
||||
state: TrackState,
|
||||
) -> Result<()> {
|
||||
let message = OscMessage::TrackStateChanged { column, row, state };
|
||||
match self.sender.try_send(message) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_column_changed(&self, column: usize) -> Result<()> {
|
||||
let message = OscMessage::SelectedColumnChanged { column };
|
||||
match self.sender.try_send(message) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_row_changed(&self, row: usize) -> Result<()> {
|
||||
let message = OscMessage::SelectedRowChanged { row };
|
||||
match self.sender.try_send(message) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Osc {
|
||||
receiver: kanal::AsyncReceiver<OscMessage>,
|
||||
framed_socket: Framed<PlatformStream, LengthDelimitedCodec>,
|
||||
}
|
||||
|
||||
impl Osc {
|
||||
/// # Platform Notes
|
||||
/// - Unix: socket_path should be a file path (e.g., "/tmp/fcb_looper.sock")
|
||||
/// - Windows: socket_path will be converted to named pipe format (e.g., "fcb_looper" -> "\\.\pipe\fcb_looper")
|
||||
pub async fn new(socket_path: &str) -> Result<(Self, OscController)> {
|
||||
// Connect to platform-specific IPC
|
||||
let stream = Self::connect_platform_stream(socket_path).await?;
|
||||
|
||||
// Wrap with length-delimited codec
|
||||
let framed_socket = Framed::new(stream, LengthDelimitedCodec::new());
|
||||
|
||||
// Create communication channel
|
||||
let (sender, receiver) = kanal::bounded(64);
|
||||
let receiver = receiver.to_async();
|
||||
|
||||
let controller = OscController { sender };
|
||||
|
||||
let server = Self {
|
||||
receiver,
|
||||
framed_socket,
|
||||
};
|
||||
|
||||
Ok((server, controller))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn connect_platform_stream(socket_path: &str) -> Result<PlatformStream> {
|
||||
UnixStream::connect(socket_path)
|
||||
.await
|
||||
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn connect_platform_stream(pipe_name: &str) -> Result<PlatformStream> {
|
||||
ClientOptions::new()
|
||||
.open(pipe_name)
|
||||
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
while let Ok(message) = self.receiver.recv().await {
|
||||
if let Err(e) = self.send_osc_message(message).await {
|
||||
log::error!("Failed to send OSC message: {}", e);
|
||||
// Continue processing other messages
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_osc_message(&mut self, message: OscMessage) -> Result<()> {
|
||||
let osc_packet = self.message_to_osc_packet(message)?;
|
||||
|
||||
let osc_bytes = rosc::encoder::encode(&osc_packet)
|
||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||
|
||||
self.framed_socket
|
||||
.send(bytes::Bytes::from(osc_bytes))
|
||||
.await
|
||||
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn message_to_osc_packet(&self, message: OscMessage) -> Result<rosc::OscPacket> {
|
||||
match message {
|
||||
OscMessage::TrackStateChanged { column, row, state } => {
|
||||
let osc_state = self.track_state_to_osc_string(state);
|
||||
let address = format!("/looper/cell/{}/{}/state", column + 1, row + 1); // 1-based indexing
|
||||
|
||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||
addr: address,
|
||||
args: vec![rosc::OscType::String(osc_state)],
|
||||
}))
|
||||
}
|
||||
OscMessage::SelectedColumnChanged { column } => {
|
||||
let address = "/looper/selected/column".to_string();
|
||||
|
||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||
addr: address,
|
||||
args: vec![rosc::OscType::Int((column + 1) as i32)], // 1-based indexing
|
||||
}))
|
||||
}
|
||||
OscMessage::SelectedRowChanged { row } => {
|
||||
let address = "/looper/selected/row".to_string();
|
||||
|
||||
Ok(rosc::OscPacket::Message(rosc::OscMessage {
|
||||
addr: address,
|
||||
args: vec![rosc::OscType::Int((row + 1) as i32)], // 1-based indexing
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn track_state_to_osc_string(&self, state: TrackState) -> String {
|
||||
match state {
|
||||
TrackState::Empty => "empty".to_string(),
|
||||
TrackState::Idle => "ready".to_string(), // Assuming track has audio data
|
||||
TrackState::Recording | TrackState::RecordingAutoStop { .. } => "recording".to_string(),
|
||||
TrackState::Playing => "playing".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,13 @@ pub struct PersistenceManager {
|
||||
|
||||
impl PersistenceManager {
|
||||
pub fn new(
|
||||
args: &Args,
|
||||
notification_rx: broadcast::Receiver<JackNotification>,
|
||||
) -> (Self, watch::Receiver<State>) {
|
||||
let state_file_path = Self::get_state_file_path();
|
||||
let state_file_path = args.config_file.clone();
|
||||
if let Some(parent) = state_file_path.parent() {
|
||||
std::fs::create_dir_all(parent).ok(); // Create directory if it doesn't exist
|
||||
}
|
||||
let initial_state = Self::load_from_disk(&state_file_path).unwrap_or_default();
|
||||
let (state_tx, state_rx) = watch::channel(initial_state.clone());
|
||||
|
||||
@ -96,14 +100,6 @@ impl PersistenceManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_state_file_path() -> PathBuf {
|
||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
path.push(".fcb_looper");
|
||||
std::fs::create_dir_all(&path).ok(); // Create directory if it doesn't exist
|
||||
path.push("state.json");
|
||||
path
|
||||
}
|
||||
|
||||
fn load_from_disk(path: &PathBuf) -> Result<State> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
|
||||
@ -4,7 +4,8 @@ const COLS: usize = 5;
|
||||
const ROWS: usize = 5;
|
||||
|
||||
pub struct ProcessHandler<F: ChunkFactory> {
|
||||
pub ports: JackPorts,
|
||||
osc: OscController,
|
||||
ports: JackPorts,
|
||||
metronome: Metronome,
|
||||
track_matrix: TrackMatrix<F, COLS, ROWS>,
|
||||
selected_row: usize,
|
||||
@ -18,6 +19,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
chunk_factory: F,
|
||||
beep_samples: Arc<AudioChunk>,
|
||||
state: &State,
|
||||
osc: OscController,
|
||||
post_record_controller: PostRecordController,
|
||||
) -> Result<Self> {
|
||||
let track_matrix = TrackMatrix::new(
|
||||
@ -27,6 +29,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
post_record_controller,
|
||||
)?;
|
||||
Ok(Self {
|
||||
osc,
|
||||
ports,
|
||||
metronome: Metronome::new(beep_samples, state),
|
||||
track_matrix,
|
||||
@ -35,6 +38,10 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ports(&self) -> &JackPorts {
|
||||
&self.ports
|
||||
}
|
||||
|
||||
pub fn handle_button_1(&mut self) -> Result<()> {
|
||||
self.track_matrix.handle_record_button(self.selected_column, self.selected_row)
|
||||
}
|
||||
@ -57,26 +64,31 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
|
||||
pub fn handle_button_6(&mut self) -> Result<()> {
|
||||
self.selected_column = 0;
|
||||
self.osc.selected_column_changed(self.selected_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_7(&mut self) -> Result<()> {
|
||||
self.selected_column = 1;
|
||||
self.osc.selected_column_changed(self.selected_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_8(&mut self) -> Result<()> {
|
||||
self.selected_column = 2;
|
||||
self.osc.selected_column_changed(self.selected_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_9(&mut self) -> Result<()> {
|
||||
self.selected_column = 3;
|
||||
self.osc.selected_column_changed(self.selected_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_10(&mut self) -> Result<()> {
|
||||
self.selected_column = 4;
|
||||
self.osc.selected_column_changed(self.selected_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -86,11 +98,13 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
} else {
|
||||
self.selected_row -= 1;
|
||||
}
|
||||
self.osc.selected_row_changed(self.selected_row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_button_down(&mut self) -> Result<()> {
|
||||
self.selected_row = (self.selected_row + 1) % ROWS;
|
||||
self.osc.selected_row_changed(self.selected_row)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -124,6 +138,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
ps,
|
||||
&mut self.ports,
|
||||
&timing,
|
||||
&mut self.osc,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -8,7 +8,7 @@ pub struct Track {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum TrackState {
|
||||
pub enum TrackState {
|
||||
Empty,
|
||||
Idle,
|
||||
Playing,
|
||||
@ -83,15 +83,17 @@ impl Track {
|
||||
self.audio_data.set_consolidated_buffer(buffer)
|
||||
}
|
||||
|
||||
pub fn handle_xrun<H>(
|
||||
pub fn handle_xrun<PRC, OC>(
|
||||
&mut self,
|
||||
beat_in_missed: Option<u32>,
|
||||
missed_frames: usize,
|
||||
chunk_factory: &mut impl ChunkFactory,
|
||||
post_record_handler: H,
|
||||
post_record_controller: PRC,
|
||||
osc_controller: OC,
|
||||
) -> Result<()>
|
||||
where
|
||||
H: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
||||
PRC: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
||||
OC: Fn(TrackState) -> Result<()>,
|
||||
{
|
||||
match beat_in_missed {
|
||||
None => {
|
||||
@ -109,7 +111,7 @@ impl Track {
|
||||
.append_silence(beat_offset as _, chunk_factory)?;
|
||||
}
|
||||
// Apply state transition at beat boundary
|
||||
self.apply_state_transition(chunk_factory, post_record_handler)?;
|
||||
self.apply_state_transition(chunk_factory, post_record_controller, osc_controller)?;
|
||||
// Insert silence after beat with new state
|
||||
let frames_after_beat = missed_frames - beat_offset as usize;
|
||||
if frames_after_beat > 0 && self.is_recording() {
|
||||
@ -122,17 +124,19 @@ impl Track {
|
||||
}
|
||||
|
||||
/// Audio processing
|
||||
pub fn process<H>(
|
||||
pub fn process<PRC, OC>(
|
||||
&mut self,
|
||||
playback_position: usize,
|
||||
beat_in_buffer: Option<u32>,
|
||||
input_buffer: &[f32],
|
||||
output_buffer: &mut [f32],
|
||||
chunk_factory: &mut impl ChunkFactory,
|
||||
post_record_handler: H,
|
||||
post_record_controller: PRC,
|
||||
osc_controller: OC,
|
||||
) -> Result<()>
|
||||
where
|
||||
H: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
||||
PRC: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
||||
OC: Fn(TrackState) -> Result<()>,
|
||||
{
|
||||
match beat_in_buffer {
|
||||
None => {
|
||||
@ -156,7 +160,7 @@ impl Track {
|
||||
}
|
||||
|
||||
// Apply state transition at beat boundary
|
||||
self.apply_state_transition(chunk_factory, post_record_handler)?;
|
||||
self.apply_state_transition(chunk_factory, post_record_controller, osc_controller)?;
|
||||
|
||||
// Process samples after beat with new current state
|
||||
if (beat_index_in_buffer as usize) < output_buffer.len() {
|
||||
@ -235,13 +239,15 @@ impl Track {
|
||||
|
||||
/// Apply state transition from next_state to current_state
|
||||
/// Returns true if track should be consolidated and saved
|
||||
fn apply_state_transition<H>(
|
||||
fn apply_state_transition<PRC, OC>(
|
||||
&mut self,
|
||||
chunk_factory: &mut impl ChunkFactory,
|
||||
post_record_handler: H,
|
||||
post_record_controller: PRC,
|
||||
osc_controller: OC,
|
||||
) -> Result<()>
|
||||
where
|
||||
H: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
||||
PRC: Fn(Arc<AudioChunk>, usize) -> Result<()>,
|
||||
OC: Fn(TrackState) -> Result<()>,
|
||||
{
|
||||
// Check for auto-stop recording completion and transition to playing if no other state transition
|
||||
if self.current_state == self.next_state {
|
||||
@ -286,12 +292,15 @@ impl Track {
|
||||
}
|
||||
|
||||
// Apply the state transition
|
||||
if self.current_state != self.next_state {
|
||||
osc_controller(self.next_state.clone())?;
|
||||
}
|
||||
self.current_state = self.next_state.clone();
|
||||
|
||||
// Handle post-record processing
|
||||
if was_recording && !self.is_recording() {
|
||||
let (chunk, sync_offset) = self.audio_data.get_chunk_for_processing()?;
|
||||
post_record_handler(chunk, sync_offset)?;
|
||||
post_record_controller(chunk, sync_offset)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
||||
ps: &jack::ProcessScope,
|
||||
ports: &mut JackPorts,
|
||||
timing: &BufferTiming,
|
||||
osc_controller: &mut OscController,
|
||||
) -> Result<()> {
|
||||
// Check for consolidation response
|
||||
if let Some(response) = self.post_record_controller.try_recv_response() {
|
||||
@ -63,6 +64,9 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
||||
client.sample_rate(),
|
||||
)
|
||||
},
|
||||
|row, state| {
|
||||
osc_controller.track_state_changed(i, row, state)
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@ -88,6 +92,9 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
|
||||
client.sample_rate(),
|
||||
)
|
||||
},
|
||||
|row, state| {
|
||||
osc_controller.track_state_changed(i, row, state)
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user