xtasks
This commit is contained in:
parent
7b7ebb8c0e
commit
470089ae5b
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
||||||
|
x = "run --package xtask --"
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
collect.txt
|
collect_*.txt
|
||||||
config/
|
config/
|
||||||
target/
|
target/
|
||||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -3018,6 +3018,15 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simulator"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"jack",
|
||||||
|
"kanal",
|
||||||
|
"wmidi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@ -4371,6 +4380,15 @@ version = "0.8.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
|
checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
@ -4,14 +4,20 @@ members = [
|
|||||||
"audio_engine",
|
"audio_engine",
|
||||||
"gui",
|
"gui",
|
||||||
"osc",
|
"osc",
|
||||||
|
"simulator",
|
||||||
|
"xtask",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
|
clap = { version = "4", features = ["derive", "env", "string"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
jack = "0.13"
|
||||||
|
kanal = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
osc = { path = "osc" }
|
osc = { path = "osc" }
|
||||||
rosc = "0.10"
|
rosc = "0.10"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
|
wmidi = "4"
|
||||||
@ -4,21 +4,21 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["derive", "env", "string"] }
|
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
hound = "3.5"
|
hound = "3.5"
|
||||||
jack = "0.13"
|
|
||||||
kanal = "0.1"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
simple_logger = "5"
|
simple_logger = "5"
|
||||||
wmidi = "4"
|
|
||||||
|
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
jack.workspace = true
|
||||||
|
kanal.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
osc.workspace = true
|
osc.workspace = true
|
||||||
rosc.workspace = true
|
rosc.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tokio.workspace = true
|
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
wmidi.workspace = true
|
||||||
@ -26,11 +26,8 @@ impl Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_config_path() -> String {
|
fn default_config_path() -> String {
|
||||||
/*
|
|
||||||
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
path.push(".fcb_looper");
|
path.push(".fcb_looper");
|
||||||
path.push("state.json");
|
path.push("state.json");
|
||||||
path.to_string_lossy().to_string()
|
path.to_string_lossy().to_string()
|
||||||
*/
|
|
||||||
"../config".to_string()
|
|
||||||
}
|
}
|
||||||
@ -70,9 +70,26 @@ impl Message {
|
|||||||
let row: usize = parts[4].parse::<usize>().address_part_result("row")? - 1; // Convert to 0-based
|
let row: usize = parts[4].parse::<usize>().address_part_result("row")? - 1; // Convert to 0-based
|
||||||
|
|
||||||
if let Some(rosc::OscType::String(state_str)) = args.first() {
|
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::<TrackState>().unwrap_or(TrackState::Empty);
|
||||||
return Ok(Some(Message::TrackStateChanged { column, row, state }));
|
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::<usize>().address_part_result("column")? - 1; // Convert to 0-based
|
||||||
|
let row: usize = parts[4].parse::<usize>().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" {
|
} else if addr == "/looper/selected/column" {
|
||||||
if let Some(rosc::OscType::Int(column_1based)) = args.first() {
|
if let Some(rosc::OscType::Int(column_1based)) = args.first() {
|
||||||
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||||
@ -88,14 +105,4 @@ impl Message {
|
|||||||
// Unknown or unsupported message
|
// Unknown or unsupported message
|
||||||
Ok(None)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
use strum::Display;
|
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Display, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, strum::Display, strum::EnumString)]
|
||||||
pub enum TrackState {
|
pub enum TrackState {
|
||||||
Empty,
|
Empty,
|
||||||
Idle,
|
Idle,
|
||||||
|
|||||||
@ -4,6 +4,6 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jack = "0.13"
|
jack.workspace = true
|
||||||
kanal = "0.1"
|
kanal.workspace = true
|
||||||
wmidi = "4"
|
wmidi.workspace = true
|
||||||
9
xtask/Cargo.toml
Normal file
9
xtask/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tokio-util.workspace = true
|
||||||
16
xtask/src/args.rs
Normal file
16
xtask/src/args.rs
Normal file
@ -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<Command>,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
Run,
|
||||||
|
Collect,
|
||||||
|
}
|
||||||
24
xtask/src/collect.rs
Normal file
24
xtask/src/collect.rs
Normal file
@ -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");
|
||||||
|
}
|
||||||
22
xtask/src/main.rs
Normal file
22
xtask/src/main.rs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
xtask/src/run.rs
Normal file
176
xtask/src/run.rs
Normal file
@ -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<Mutex<Option<Child>>>;
|
||||||
|
|
||||||
|
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<ProcessHandle>) {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
31
xtask/src/workspace.rs
Normal file
31
xtask/src/workspace.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub async fn change_to_workspace_dir() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// 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(())
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user