diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..90e98b9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +xtask = "run --package xtask --" +x = "run --package xtask --" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8456abd..3c1ee75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -collect.txt +collect_*.txt config/ target/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 61974d1..23469a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3018,6 +3018,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "simulator" +version = "0.1.0" +dependencies = [ + "jack", + "kanal", + "wmidi", +] + [[package]] name = "slab" version = "0.4.10" @@ -4371,6 +4380,15 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "clap", + "tokio", + "tokio-util", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 5bcdfeb..468d38f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,20 @@ members = [ "audio_engine", "gui", "osc", + "simulator", + "xtask", ] [workspace.dependencies] bytes = "1.0" +clap = { version = "4", features = ["derive", "env", "string"] } futures = "0.3" +jack = "0.13" +kanal = "0.1" log = "0.4" osc = { path = "osc" } rosc = "0.10" thiserror = "1" tokio = { version = "1", features = ["full"] } -tokio-util = { version = "0.7", features = ["codec"] } \ No newline at end of file +tokio-util = { version = "0.7", features = ["codec"] } +wmidi = "4" \ No newline at end of file diff --git a/audio_engine/Cargo.toml b/audio_engine/Cargo.toml index 860f6ac..003e127 100644 --- a/audio_engine/Cargo.toml +++ b/audio_engine/Cargo.toml @@ -4,21 +4,21 @@ version = "0.1.0" edition = "2021" [dependencies] -clap = { version = "4", features = ["derive", "env", "string"] } dirs = "5" hound = "3.5" -jack = "0.13" -kanal = "0.1" serde = { version = "1", features = ["derive"] } serde_json = "1" simple_logger = "5" -wmidi = "4" bytes.workspace = true +clap.workspace = true futures.workspace = true +jack.workspace = true +kanal.workspace = true log.workspace = true osc.workspace = true rosc.workspace = true thiserror.workspace = true +tokio-util.workspace = true tokio.workspace = true -tokio-util.workspace = true \ No newline at end of file +wmidi.workspace = true \ No newline at end of file diff --git a/audio_engine/src/args.rs b/audio_engine/src/args.rs index 65a0241..f75767c 100644 --- a/audio_engine/src/args.rs +++ b/audio_engine/src/args.rs @@ -26,11 +26,8 @@ impl Args { } 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() - */ - "../config".to_string() } \ No newline at end of file diff --git a/osc/src/message.rs b/osc/src/message.rs index b51c373..cee70f7 100644 --- a/osc/src/message.rs +++ b/osc/src/message.rs @@ -70,9 +70,26 @@ impl Message { let row: usize = parts[4].parse::().address_part_result("row")? - 1; // Convert to 0-based if let Some(rosc::OscType::String(state_str)) = args.first() { - let state = Self::track_state_from_osc_string(state_str); + let state = state_str.parse::().unwrap_or(TrackState::Empty); return Ok(Some(Message::TrackStateChanged { column, row, state })); } + } else if addr.starts_with("/looper/cell/") && addr.ends_with("/volume") { + // Parse: /looper/cell/{column}/{row}/volume + let parts: Vec<&str> = addr.split('/').collect(); + if parts.len() != 6 { + return Err(Error::AddressParseError(addr)); + } + + let column: usize = parts[3].parse::().address_part_result("column")? - 1; // Convert to 0-based + let row: usize = parts[4].parse::().address_part_result("row")? - 1; // Convert to 0-based + + if let Some(rosc::OscType::Float(volume)) = args.first() { + return Ok(Some(Message::TrackVolumeChanged { + column, + row, + volume: *volume + })); + } } else if addr == "/looper/selected/column" { if let Some(rosc::OscType::Int(column_1based)) = args.first() { let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based @@ -88,14 +105,4 @@ impl Message { // Unknown or unsupported message Ok(None) } - - fn track_state_from_osc_string(s: &str) -> TrackState { - match s { - "empty" => TrackState::Empty, - "idle" => TrackState::Idle, - "recording" => TrackState::Recording, - "playing" => TrackState::Playing, - _ => TrackState::Empty, // Default fallback - } - } } \ No newline at end of file diff --git a/osc/src/state.rs b/osc/src/state.rs index f627c42..ae20014 100644 --- a/osc/src/state.rs +++ b/osc/src/state.rs @@ -1,8 +1,6 @@ -use strum::Display; - use crate::*; -#[derive(Clone, Debug, Display, PartialEq)] +#[derive(Clone, Debug, PartialEq, strum::Display, strum::EnumString)] pub enum TrackState { Empty, Idle, diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 7fb2266..9c8d0e6 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -jack = "0.13" -kanal = "0.1" -wmidi = "4" \ No newline at end of file +jack.workspace = true +kanal.workspace = true +wmidi.workspace = true \ No newline at end of file diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..9e1b144 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap.workspace = true +tokio.workspace = true +tokio-util.workspace = true diff --git a/xtask/src/args.rs b/xtask/src/args.rs new file mode 100644 index 0000000..0bd58b2 --- /dev/null +++ b/xtask/src/args.rs @@ -0,0 +1,16 @@ + use clap::*; + +#[derive(Parser)] +#[command(name = "fcb-looper")] +#[command(version, about = "xtasks for the fcb-looper")] +pub struct Args { + #[command(subcommand)] + pub command: Option, + +} + +#[derive(Subcommand)] +pub enum Command { + Run, + Collect, +} \ No newline at end of file diff --git a/xtask/src/collect.rs b/xtask/src/collect.rs new file mode 100644 index 0000000..2397267 --- /dev/null +++ b/xtask/src/collect.rs @@ -0,0 +1,24 @@ +use tokio::process::Command; + +pub async fn collect() { + collect_dir_output("audio_engine", "../collect_audio_engine.txt").await; + collect_dir_output("gui", "../collect_gui.txt").await; + collect_dir_output("osc", "../collect_osc.txt").await; + collect_dir_output("simulator", "../collect_simulator.txt").await; + collect_dir_output("xtask", "../collect_xtask.txt").await; +} + +pub async fn collect_dir_output(dir: &str, output: &str) { + #[cfg(target_os = "windows")] + let bash = "C:\\Program Files\\Git\\git-bash.exe"; + #[cfg(target_os = "linux")] + let bash = "bash"; + + Command::new(bash) + .current_dir(dir) + .arg("-c") + .arg(format!("../collect.sh -s core -o {output}")) + .output() + .await + .expect("Failed to collect audio_engine sources"); +} \ No newline at end of file diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..644a2ef --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,22 @@ +mod args; +mod collect; +mod run; +mod workspace; + +use clap::Parser; + +#[tokio::main] +async fn main() { + let args = args::Args::parse(); + + workspace::change_to_workspace_dir().await.expect("Failed to change to workspace directory"); + + match args.command { + Some(args::Command::Run) | None => { + run::run().await; + } + Some(args::Command::Collect) => { + collect::collect().await; + } + } +} \ No newline at end of file diff --git a/xtask/src/run.rs b/xtask/src/run.rs new file mode 100644 index 0000000..39873b7 --- /dev/null +++ b/xtask/src/run.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; +use tokio::process::{Child, Command}; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +type ProcessHandle = Arc>>; + +pub async fn run() { + let qjackctl_handle: ProcessHandle = Arc::new(Mutex::new(None)); + let audio_engine_handle: ProcessHandle = Arc::new(Mutex::new(None)); + let gui_handle: ProcessHandle = Arc::new(Mutex::new(None)); + let simulator_handle: ProcessHandle = Arc::new(Mutex::new(None)); + + let cancel_token = CancellationToken::new(); + + // Set up signal handling for graceful shutdown + let cleanup_handles = vec![ + qjackctl_handle.clone(), + audio_engine_handle.clone(), + gui_handle.clone(), + simulator_handle.clone(), + ]; + let cleanup_token = cancel_token.clone(); + + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl+c"); + println!("Received Ctrl+C, shutting down..."); + cleanup_token.cancel(); + cleanup_processes(cleanup_handles).await; + stop_jack_daemon().await; + }); + + // Start processes + let qjackctl = spawn_qjackctl().await; + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + let audio_engine = spawn_audio_engine().await; + let gui = spawn_gui().await; + let simulator = spawn_simulator().await; + + // Store handles for cleanup + *qjackctl_handle.lock().await = Some(qjackctl); + *audio_engine_handle.lock().await = Some(audio_engine); + *gui_handle.lock().await = Some(gui); + *simulator_handle.lock().await = Some(simulator); + + // Get references back for waiting + let mut qjackctl = qjackctl_handle.lock().await.take().unwrap(); + let mut audio_engine = audio_engine_handle.lock().await.take().unwrap(); + let mut gui = gui_handle.lock().await.take().unwrap(); + let mut simulator = simulator_handle.lock().await.take().unwrap(); + + // Wait for any process to exit or cancellation + tokio::select! { + result = qjackctl.wait() => { + println!("qjackctl exited: {:?}", result); + kill_process(&mut audio_engine, "audio_engine").await; + kill_process(&mut gui, "gui").await; + kill_process(&mut simulator, "simulator").await; + stop_jack_daemon().await; + } + result = audio_engine.wait() => { + println!("audio_engine exited: {:?}", result); + kill_process(&mut qjackctl, "qjackctl").await; + kill_process(&mut gui, "gui").await; + kill_process(&mut simulator, "simulator").await; + stop_jack_daemon().await; + } + result = gui.wait() => { + println!("gui exited: {:?}", result); + kill_process(&mut qjackctl, "qjackctl").await; + kill_process(&mut audio_engine, "audio_engine").await; + kill_process(&mut simulator, "simulator").await; + stop_jack_daemon().await; + } + result = simulator.wait() => { + println!("simulator exited: {:?}", result); + kill_process(&mut qjackctl, "qjackctl").await; + kill_process(&mut audio_engine, "audio_engine").await; + kill_process(&mut gui, "gui").await; + stop_jack_daemon().await; + } + _ = cancel_token.cancelled() => { + println!("Shutdown requested"); + kill_process(&mut qjackctl, "qjackctl").await; + kill_process(&mut audio_engine, "audio_engine").await; + kill_process(&mut gui, "gui").await; + kill_process(&mut simulator, "simulator").await; + stop_jack_daemon().await; + } + } + + println!("All processes stopped"); +} + +async fn cleanup_processes(handles: Vec) { + for handle in handles { + if let Some(mut child) = handle.lock().await.take() { + let _ = child.kill().await; + } + } +} + +async fn stop_jack_daemon() { + println!("Stopping JACK daemon..."); + + #[cfg(target_os = "windows")] + let (jack, args) = ("taskkill", vec!["/f", "/im", "jackd.exe"]); + + #[cfg(target_os = "linux")] + let (jack, args) = ("pkill", vec!["-f", "jackd"]); + + Command::new(jack).args(args).output().await.expect("Failed to stop JACK daemon"); +} + +async fn spawn_qjackctl() -> Child { + #[cfg(target_os = "windows")] + let qjackctl = "C:\\Program Files\\JACK2\\qjackctl\\qjackctl.exe"; + #[cfg(target_os = "linux")] + let qjackctl = "qjackctl"; + + #[cfg(target_os = "windows")] + let qjackctl_arg = "/start-server"; + #[cfg(target_os = "linux")] + let qjackctl_arg = "--start-server"; + + Command::new(qjackctl) + .arg(qjackctl_arg) + .spawn() + .expect("Could not start qjackctl") +} + +async fn spawn_audio_engine() -> Child { + Command::new("cargo") + .args([ + "run", + "--release", + "--bin", + "audio_engine", + "--", + "--config-path", + "config", + ]) + .spawn() + .expect("Could not start audio engine") +} + +async fn spawn_gui() -> Child { + Command::new("cargo") + .args([ + "run", + "--release", + "--bin", + "gui", + ]) + .spawn() + .expect("Could not start gui") +} + +async fn spawn_simulator() -> Child { + Command::new("cargo") + .args([ + "run", + "--release", + "--bin", + "simulator", + ]) + .spawn() + .expect("Could not start simulator") +} + +async fn kill_process(child: &mut Child, name: &str) { + match child.kill().await { + Ok(()) => println!("Killed {}", name), + Err(e) => eprintln!("Failed to kill {}: {}", name, e), + } +} \ No newline at end of file diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs new file mode 100644 index 0000000..21ba63d --- /dev/null +++ b/xtask/src/workspace.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; +use tokio::process::Command; + +pub async fn change_to_workspace_dir() -> Result<(), Box> { + // Use cargo to find the workspace root + let output = Command::new("cargo") + .args(["locate-project", "--workspace", "--message-format=plain"]) + .output() + .await?; + + if !output.status.success() { + return Err(format!( + "Failed to locate workspace: {}", + String::from_utf8_lossy(&output.stderr) + ).into()); + } + + let workspace_cargo_toml = String::from_utf8(output.stdout)?; + let workspace_cargo_toml = workspace_cargo_toml.trim(); + + // Get the directory containing Cargo.toml + let workspace_dir = PathBuf::from(workspace_cargo_toml) + .parent() + .ok_or("Failed to get workspace directory")? + .to_path_buf(); + + println!("Changing to workspace directory: {}", workspace_dir.display()); + std::env::set_current_dir(&workspace_dir)?; + + Ok(()) +} \ No newline at end of file