Save and restore connections
This commit is contained in:
parent
7eec4f0093
commit
b296b752b3
80
Cargo.lock
generated
80
Cargo.lock
generated
@ -81,12 +81,44 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@ -158,6 +190,16 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@ -178,9 +220,12 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
|||||||
name = "looper"
|
name = "looper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
"jack",
|
"jack",
|
||||||
"kanal",
|
"kanal",
|
||||||
"log",
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -237,6 +282,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@ -305,12 +356,29 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -337,6 +405,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.140"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.5"
|
version = "1.4.5"
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@ -14,10 +14,13 @@ name = "sendmidi"
|
|||||||
path = "src/sendmidi.rs"
|
path = "src/sendmidi.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1"
|
dirs = "5"
|
||||||
jack = "0.13"
|
jack = "0.13"
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
log = "0.4"
|
|
||||||
simple_logger = "5"
|
|
||||||
kanal = "0.1"
|
kanal = "0.1"
|
||||||
wmidi = "4"
|
log = "0.4"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
simple_logger = "5"
|
||||||
|
thiserror = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
wmidi = "4"
|
||||||
@ -218,6 +218,15 @@ For efficiency, column-beat messages are bundled:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
|
"connections": {
|
||||||
|
"midi_in": [
|
||||||
|
"sendmidi:midi_out"
|
||||||
|
],
|
||||||
|
"audio_out": [
|
||||||
|
],
|
||||||
|
"audio_in": [
|
||||||
|
]
|
||||||
|
},
|
||||||
"ui_state": {
|
"ui_state": {
|
||||||
"selected_column": 3,
|
"selected_column": 3,
|
||||||
"selected_row": 2
|
"selected_row": 2
|
||||||
|
|||||||
@ -5,13 +5,11 @@ pub struct Allocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Allocator {
|
impl Allocator {
|
||||||
pub fn spawn(buffer_size: usize, pool_size: usize) -> (Self, tokio::task::JoinHandle<()>) {
|
pub fn spawn(buffer_size: usize, pool_size: usize) -> Self {
|
||||||
let (allocated_buffer_sender, allocated_buffer_receiver) = kanal::bounded(pool_size);
|
let (allocated_buffer_sender, allocated_buffer_receiver) = kanal::bounded(pool_size);
|
||||||
|
|
||||||
// Pre-fill the channel with initial buffers
|
// Pre-fill the channel with initial buffers
|
||||||
while let Ok(true) = allocated_buffer_sender
|
while let Ok(true) = allocated_buffer_sender.try_send(AudioChunk::allocate(buffer_size)) {}
|
||||||
.try_send(AudioChunk::allocate(buffer_size))
|
|
||||||
{}
|
|
||||||
|
|
||||||
let allocator = Allocator {
|
let allocator = Allocator {
|
||||||
channel: allocated_buffer_receiver,
|
channel: allocated_buffer_receiver,
|
||||||
@ -20,7 +18,7 @@ impl Allocator {
|
|||||||
let allocated_buffer_sender = allocated_buffer_sender.to_async();
|
let allocated_buffer_sender = allocated_buffer_sender.to_async();
|
||||||
|
|
||||||
// Spawn the background task that continuously allocates buffers
|
// Spawn the background task that continuously allocates buffers
|
||||||
let join_handle = tokio::runtime::Handle::current().spawn(async move {
|
tokio::runtime::Handle::current().spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let chunk = AudioChunk::allocate(buffer_size);
|
let chunk = AudioChunk::allocate(buffer_size);
|
||||||
if allocated_buffer_sender.send(chunk).await.is_err() {
|
if allocated_buffer_sender.send(chunk).await.is_err() {
|
||||||
@ -29,7 +27,7 @@ impl Allocator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
(allocator, join_handle)
|
allocator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -553,7 +553,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(chunk.sample_count, 3); // First chunk unchanged
|
assert_eq!(chunk.sample_count, 3); // First chunk unchanged
|
||||||
|
|
||||||
let chunk2 = chunk.next.as_ref().unwrap();
|
let chunk2 = chunk.next.as_ref().unwrap();
|
||||||
assert_eq!(chunk2.sample_count, 2);
|
assert_eq!(chunk2.sample_count, 2);
|
||||||
assert_eq!(chunk2.samples[0], 4.0);
|
assert_eq!(chunk2.samples[0], 4.0);
|
||||||
@ -587,7 +587,7 @@ mod tests {
|
|||||||
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
|
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
|
||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
// Navigate to chunk3 and verify the sample was added
|
// Navigate to chunk3 and verify the sample was added
|
||||||
let chunk2 = chunk1.next.as_ref().unwrap();
|
let chunk2 = chunk1.next.as_ref().unwrap();
|
||||||
let chunk3 = chunk2.next.as_ref().unwrap();
|
let chunk3 = chunk2.next.as_ref().unwrap();
|
||||||
@ -600,7 +600,7 @@ mod tests {
|
|||||||
let mut chunk = AudioChunk::allocate(5);
|
let mut chunk = AudioChunk::allocate(5);
|
||||||
|
|
||||||
let mut factory = || panic!("Factory should not be called");
|
let mut factory = || panic!("Factory should not be called");
|
||||||
|
|
||||||
// First append
|
// First append
|
||||||
let result1 = AudioChunk::append_samples(&mut chunk, &[1.0, 2.0], &mut factory);
|
let result1 = AudioChunk::append_samples(&mut chunk, &[1.0, 2.0], &mut factory);
|
||||||
assert!(result1.is_ok());
|
assert!(result1.is_ok());
|
||||||
@ -644,12 +644,12 @@ mod tests {
|
|||||||
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
|
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
|
||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
// Check that chunk2 got filled
|
// Check that chunk2 got filled
|
||||||
let chunk2 = chunk1.next.as_ref().unwrap();
|
let chunk2 = chunk1.next.as_ref().unwrap();
|
||||||
assert_eq!(chunk2.sample_count, 3);
|
assert_eq!(chunk2.sample_count, 3);
|
||||||
assert_eq!(chunk2.samples[2], 6.0);
|
assert_eq!(chunk2.samples[2], 6.0);
|
||||||
|
|
||||||
// Check that chunk3 was created with remaining samples
|
// Check that chunk3 was created with remaining samples
|
||||||
let chunk3 = chunk2.next.as_ref().unwrap();
|
let chunk3 = chunk2.next.as_ref().unwrap();
|
||||||
assert_eq!(chunk3.sample_count, 2);
|
assert_eq!(chunk3.sample_count, 2);
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
mod allocator;
|
mod allocator;
|
||||||
mod audio_chunk;
|
mod audio_chunk;
|
||||||
mod chunk_factory;
|
mod chunk_factory;
|
||||||
|
mod connection_manager;
|
||||||
mod looper_error;
|
mod looper_error;
|
||||||
mod midi;
|
mod midi;
|
||||||
mod notification_handler;
|
mod notification_handler;
|
||||||
mod process_handler;
|
mod process_handler;
|
||||||
|
mod state;
|
||||||
|
mod state_manager;
|
||||||
mod track;
|
mod track;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -12,54 +15,76 @@ use std::sync::Arc;
|
|||||||
use allocator::Allocator;
|
use allocator::Allocator;
|
||||||
use audio_chunk::AudioChunk;
|
use audio_chunk::AudioChunk;
|
||||||
use chunk_factory::ChunkFactory;
|
use chunk_factory::ChunkFactory;
|
||||||
|
use connection_manager::ConnectionManager;
|
||||||
use looper_error::LooperError;
|
use looper_error::LooperError;
|
||||||
use looper_error::Result;
|
use looper_error::Result;
|
||||||
use notification_handler::JackNotification;
|
use notification_handler::JackNotification;
|
||||||
use notification_handler::NotificationHandler;
|
use notification_handler::NotificationHandler;
|
||||||
use process_handler::ProcessHandler;
|
use process_handler::ProcessHandler;
|
||||||
|
use state::State;
|
||||||
|
use state_manager::StateManager;
|
||||||
use track::Track;
|
use track::Track;
|
||||||
use track::TrackState;
|
use track::TrackState;
|
||||||
|
|
||||||
pub struct JackPorts {
|
pub struct JackPorts {
|
||||||
pub audio_in: jack::Port<jack::AudioIn>,
|
pub audio_in: jack::Port<jack::AudioIn>,
|
||||||
pub audio_out: jack::Port<jack::AudioOut>,
|
pub audio_out: jack::Port<jack::AudioOut>,
|
||||||
pub midi_in: jack::Port<jack::MidiIn>,
|
pub midi_in: jack::Port<jack::MidiIn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
simple_logger::SimpleLogger::new().init().expect("Could not initialize logger");
|
simple_logger::SimpleLogger::new()
|
||||||
|
.init()
|
||||||
|
.expect("Could not initialize logger");
|
||||||
|
|
||||||
let (jack_client, ports) = setup_jack();
|
let (jack_client, ports) = setup_jack();
|
||||||
|
|
||||||
let (allocator, factory_handle) = Allocator::spawn(jack_client.sample_rate(), 3);
|
let allocator = Allocator::spawn(jack_client.sample_rate(), 3);
|
||||||
|
|
||||||
let process_handler = ProcessHandler::new(ports, allocator)
|
let process_handler =
|
||||||
.expect("Could not create process handler");
|
ProcessHandler::new(ports, allocator).expect("Could not create process handler");
|
||||||
|
|
||||||
let notification_handler = NotificationHandler::new();
|
let notification_handler = NotificationHandler::new();
|
||||||
let mut notification_channel = notification_handler.subscribe();
|
let mut notification_channel = notification_handler.subscribe();
|
||||||
|
|
||||||
let _async_client = jack_client
|
let (mut state_manager, state_rx) = StateManager::new(notification_handler.subscribe());
|
||||||
|
|
||||||
|
let mut connection_manager = ConnectionManager::new(
|
||||||
|
state_rx,
|
||||||
|
notification_handler.subscribe(),
|
||||||
|
jack_client.name().to_string(),
|
||||||
|
)
|
||||||
|
.expect("Could not create connection manager");
|
||||||
|
|
||||||
|
let _active_client = jack_client
|
||||||
.activate_async(notification_handler, process_handler)
|
.activate_async(notification_handler, process_handler)
|
||||||
.expect("Could not activate Jack");
|
.expect("Could not activate Jack");
|
||||||
|
|
||||||
tokio::select! {
|
loop {
|
||||||
notification = notification_channel.recv() => {
|
tokio::select! {
|
||||||
match notification {
|
notification = notification_channel.recv() => {
|
||||||
Ok(JackNotification::Shutdown {reason, status}) => {
|
if let Ok(JackNotification::Shutdown {reason, status}) = notification {
|
||||||
log::error!("Jack shutdown: {reason} {status:?}");
|
log::error!("Jack shutdown: {reason} {status:?}");
|
||||||
}
|
break;
|
||||||
Err(e) => {
|
|
||||||
log::error!("NotificationHandler stopped: {e}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
result = state_manager.run() => {
|
||||||
_ = factory_handle => {
|
if let Err(e) = result {
|
||||||
log::error!("Allocator was dropped");
|
log::error!("StateManager task failed: {}", e);
|
||||||
}
|
}
|
||||||
_ = tokio::signal::ctrl_c() => {
|
break;
|
||||||
log::info!("Stopping");
|
}
|
||||||
|
result = connection_manager.run() => {
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("ConnectionManager task failed: {}", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
log::info!("Stopping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,12 +105,12 @@ fn setup_jack() -> (jack::Client, JackPorts) {
|
|||||||
let midi_in = jack_client
|
let midi_in = jack_client
|
||||||
.register_port("midi_in", jack::MidiIn::default())
|
.register_port("midi_in", jack::MidiIn::default())
|
||||||
.expect("Could not create midi_in port");
|
.expect("Could not create midi_in port");
|
||||||
|
|
||||||
let ports = JackPorts {
|
let ports = JackPorts {
|
||||||
audio_in,
|
audio_in,
|
||||||
audio_out,
|
audio_out,
|
||||||
midi_in,
|
midi_in,
|
||||||
};
|
};
|
||||||
|
|
||||||
(jack_client, ports)
|
(jack_client, ports)
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/connection_manager.rs
Normal file
80
src/connection_manager.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use crate::*;
|
||||||
|
use tokio::sync::{broadcast, watch};
|
||||||
|
|
||||||
|
pub struct ConnectionManager {
|
||||||
|
state_rx: watch::Receiver<State>,
|
||||||
|
notification_rx: broadcast::Receiver<JackNotification>,
|
||||||
|
jack_client: jack::Client,
|
||||||
|
jack_client_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionManager {
|
||||||
|
pub fn new(
|
||||||
|
state_rx: watch::Receiver<State>,
|
||||||
|
notification_rx: broadcast::Receiver<JackNotification>,
|
||||||
|
jack_client_name: String,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// Create a dedicated client for making connections
|
||||||
|
let (jack_client, _status) = jack::Client::new(
|
||||||
|
&format!("{}_connector", jack_client_name),
|
||||||
|
jack::ClientOptions::NO_START_SERVER,
|
||||||
|
)
|
||||||
|
.map_err(|_| LooperError::JackConnection(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
state_rx,
|
||||||
|
notification_rx,
|
||||||
|
jack_client,
|
||||||
|
jack_client_name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
self.restore_connections().await;
|
||||||
|
while let Ok(notification) = self.notification_rx.recv().await {
|
||||||
|
if let JackNotification::PortRegistered { .. } = notification {
|
||||||
|
self.restore_connections().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_connections(&self) {
|
||||||
|
let state = self.state_rx.borrow();
|
||||||
|
|
||||||
|
// MIDI and audio inputs: connect FROM external TO our port
|
||||||
|
for external_port in &state.connections.midi_in {
|
||||||
|
let our_port = format!("{}:midi_in", self.jack_client_name);
|
||||||
|
let result = self
|
||||||
|
.jack_client
|
||||||
|
.connect_ports_by_name(external_port, &our_port);
|
||||||
|
match result {
|
||||||
|
Ok(_) => log::info!("Connected {} -> {}", external_port, our_port),
|
||||||
|
Err(_) => log::debug!("Could not connect {} -> {}", external_port, our_port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for external_port in &state.connections.audio_in {
|
||||||
|
let our_port = format!("{}:audio_in", self.jack_client_name);
|
||||||
|
let result = self
|
||||||
|
.jack_client
|
||||||
|
.connect_ports_by_name(external_port, &our_port);
|
||||||
|
match result {
|
||||||
|
Ok(_) => log::info!("Connected {} -> {}", external_port, our_port),
|
||||||
|
Err(_) => log::debug!("Could not connect {} -> {}", external_port, our_port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio output: connect FROM our port TO external
|
||||||
|
for external_port in &state.connections.audio_out {
|
||||||
|
let our_port = format!("{}:audio_out", self.jack_client_name);
|
||||||
|
let result = self
|
||||||
|
.jack_client
|
||||||
|
.connect_ports_by_name(&our_port, external_port);
|
||||||
|
match result {
|
||||||
|
Ok(_) => log::info!("Connected {} -> {}", our_port, external_port),
|
||||||
|
Err(_) => log::debug!("Could not connect {} -> {}", our_port, external_port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,15 @@ pub enum LooperError {
|
|||||||
|
|
||||||
#[error("Index out of bounds")]
|
#[error("Index out of bounds")]
|
||||||
OutOfBounds(&'static std::panic::Location<'static>),
|
OutOfBounds(&'static std::panic::Location<'static>),
|
||||||
|
|
||||||
|
#[error("Failed to load state")]
|
||||||
|
StateLoad(&'static std::panic::Location<'static>),
|
||||||
|
|
||||||
|
#[error("Failed to save state")]
|
||||||
|
StateSave(&'static std::panic::Location<'static>),
|
||||||
|
|
||||||
|
#[error("Failed to connect to JACK")]
|
||||||
|
JackConnection(&'static std::panic::Location<'static>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, LooperError>;
|
pub type Result<T> = std::result::Result<T, LooperError>;
|
||||||
|
|||||||
10
src/midi.rs
10
src/midi.rs
@ -10,7 +10,7 @@ pub fn process_events<F: ChunkFactory>(
|
|||||||
const MAX_EVENTS: usize = 16; // Reasonable limit for real-time processing
|
const MAX_EVENTS: usize = 16; // Reasonable limit for real-time processing
|
||||||
let mut raw_events = [[0u8; 3]; MAX_EVENTS];
|
let mut raw_events = [[0u8; 3]; MAX_EVENTS];
|
||||||
let mut event_count = 0;
|
let mut event_count = 0;
|
||||||
|
|
||||||
// Collect events from the MIDI input iterator
|
// 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 {
|
for midi_event in midi_input {
|
||||||
@ -23,12 +23,12 @@ pub fn process_events<F: ChunkFactory>(
|
|||||||
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
|
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now process the collected events using wmidi
|
// Now process the collected events using wmidi
|
||||||
// The iterator borrow is dropped, so we can mutably borrow process_handler
|
// The iterator borrow is dropped, so we can mutably borrow process_handler
|
||||||
for i in 0..event_count {
|
for i in 0..event_count {
|
||||||
let event_bytes = &raw_events[i];
|
let event_bytes = &raw_events[i];
|
||||||
|
|
||||||
// Use wmidi for cleaner MIDI parsing instead of manual byte checking
|
// Use wmidi for cleaner MIDI parsing instead of manual byte checking
|
||||||
match wmidi::MidiMessage::try_from(&event_bytes[..]) {
|
match wmidi::MidiMessage::try_from(&event_bytes[..]) {
|
||||||
Ok(message) => {
|
Ok(message) => {
|
||||||
@ -62,6 +62,6 @@ pub fn process_events<F: ChunkFactory>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,18 +4,24 @@ pub struct NotificationHandler {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum JackNotification {
|
pub enum JackNotification {
|
||||||
Shutdown{
|
Shutdown {
|
||||||
status: jack::ClientStatus,
|
status: jack::ClientStatus,
|
||||||
reason: String
|
reason: String,
|
||||||
|
},
|
||||||
|
PortConnect {
|
||||||
|
port_a: String,
|
||||||
|
port_b: String,
|
||||||
|
connected: bool,
|
||||||
|
},
|
||||||
|
PortRegistered {
|
||||||
|
port_name: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NotificationHandler {
|
impl NotificationHandler {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (channel, _) = tokio::sync::broadcast::channel(1);
|
let (channel, _) = tokio::sync::broadcast::channel(16);
|
||||||
Self {
|
Self { channel }
|
||||||
channel,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<JackNotification> {
|
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<JackNotification> {
|
||||||
@ -26,6 +32,52 @@ impl NotificationHandler {
|
|||||||
impl jack::NotificationHandler for NotificationHandler {
|
impl jack::NotificationHandler for NotificationHandler {
|
||||||
unsafe fn shutdown(&mut self, status: jack::ClientStatus, reason: &str) {
|
unsafe fn shutdown(&mut self, status: jack::ClientStatus, reason: &str) {
|
||||||
let reason = reason.to_string();
|
let reason = reason.to_string();
|
||||||
self.channel.send(JackNotification::Shutdown { status, reason }).expect("Could not send shutdown notification");
|
self.channel
|
||||||
|
.send(JackNotification::Shutdown { status, reason })
|
||||||
|
.expect("Could not send shutdown notification");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fn ports_connected(
|
||||||
|
&mut self,
|
||||||
|
client: &jack::Client,
|
||||||
|
port_id_a: jack::PortId,
|
||||||
|
port_id_b: jack::PortId,
|
||||||
|
connect: bool,
|
||||||
|
) {
|
||||||
|
// Convert port IDs to port names
|
||||||
|
let Some(port_a) = client.port_by_id(port_id_a) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(port_a) = port_a.name() else { return };
|
||||||
|
let Some(port_b) = client.port_by_id(port_id_b) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(port_b) = port_b.name() else { return };
|
||||||
|
|
||||||
|
let notification = JackNotification::PortConnect {
|
||||||
|
port_a,
|
||||||
|
port_b,
|
||||||
|
connected: connect,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.channel
|
||||||
|
.send(notification)
|
||||||
|
.expect("Could not send port connection notification");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_registration(&mut self, client: &jack::Client, port_id: jack::PortId, register: bool) {
|
||||||
|
let Some(port_name) = client.port_by_id(port_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(port_name) = port_name.name() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if register {
|
||||||
|
let notification = JackNotification::PortRegistered { port_name };
|
||||||
|
self.channel
|
||||||
|
.send(notification)
|
||||||
|
.expect("Could not send port registration notification");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,10 +8,7 @@ pub struct ProcessHandler<F: ChunkFactory> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory> ProcessHandler<F> {
|
impl<F: ChunkFactory> ProcessHandler<F> {
|
||||||
pub fn new(
|
pub fn new(ports: JackPorts, mut chunk_factory: F) -> Result<Self> {
|
||||||
ports: JackPorts,
|
|
||||||
mut chunk_factory: F,
|
|
||||||
) -> Result<Self> {
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track: Track::new(&mut chunk_factory)?,
|
track: Track::new(&mut chunk_factory)?,
|
||||||
playback_position: 0,
|
playback_position: 0,
|
||||||
@ -63,7 +60,7 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
|||||||
if midi::process_events(self, ps).is_err() {
|
if midi::process_events(self, ps).is_err() {
|
||||||
return jack::Control::Quit;
|
return jack::Control::Quit;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_buffer = self.ports.audio_in.as_slice(ps);
|
let input_buffer = self.ports.audio_in.as_slice(ps);
|
||||||
let output_buffer = self.ports.audio_out.as_mut_slice(ps);
|
let output_buffer = self.ports.audio_out.as_mut_slice(ps);
|
||||||
|
|
||||||
@ -117,4 +114,4 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
|||||||
}
|
}
|
||||||
jack::Control::Continue
|
jack::Control::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::io::{self, Write};
|
|
||||||
use jack::RawMidi;
|
use jack::RawMidi;
|
||||||
use kanal::{Receiver, Sender};
|
use kanal::{Receiver, Sender};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum MidiCommand {
|
enum MidiCommand {
|
||||||
@ -14,7 +14,10 @@ struct MidiSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MidiSender {
|
impl MidiSender {
|
||||||
fn new(client: &jack::Client, command_receiver: Receiver<MidiCommand>) -> Result<Self, Box<dyn std::error::Error>> {
|
fn new(
|
||||||
|
client: &jack::Client,
|
||||||
|
command_receiver: Receiver<MidiCommand>,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let midi_out = client
|
let midi_out = client
|
||||||
.register_port("midi_out", jack::MidiOut::default())
|
.register_port("midi_out", jack::MidiOut::default())
|
||||||
.map_err(|e| format!("Could not create MIDI output port: {}", e))?;
|
.map_err(|e| format!("Could not create MIDI output port: {}", e))?;
|
||||||
@ -29,20 +32,23 @@ impl MidiSender {
|
|||||||
impl jack::ProcessHandler for MidiSender {
|
impl jack::ProcessHandler for MidiSender {
|
||||||
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
||||||
let mut midi_writer = self.midi_out.writer(ps);
|
let mut midi_writer = self.midi_out.writer(ps);
|
||||||
|
|
||||||
// Process all pending commands
|
// Process all pending commands
|
||||||
while let Ok(Some(command)) = self.command_receiver.try_recv() {
|
while let Ok(Some(command)) = self.command_receiver.try_recv() {
|
||||||
match command {
|
match command {
|
||||||
MidiCommand::ControlChange { cc, value } => {
|
MidiCommand::ControlChange { cc, value } => {
|
||||||
let midi_data = [0xB0, cc, value]; // Control Change on channel 1
|
let midi_data = [0xB0, cc, value]; // Control Change on channel 1
|
||||||
if let Err(e) = midi_writer.write(&RawMidi { time: 0, bytes: &midi_data }) {
|
if let Err(e) = midi_writer.write(&RawMidi {
|
||||||
|
time: 0,
|
||||||
|
bytes: &midi_data,
|
||||||
|
}) {
|
||||||
eprintln!("Failed to send MIDI: {}", e);
|
eprintln!("Failed to send MIDI: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MidiCommand::Quit => return jack::Control::Quit,
|
MidiCommand::Quit => return jack::Control::Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jack::Control::Continue
|
jack::Control::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,37 +75,28 @@ fn show_help() {
|
|||||||
println!("q - Quit\n");
|
println!("q - Quit\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_connect(active_client: &jack::AsyncClient<(), MidiSender>) {
|
|
||||||
// Try to connect our MIDI output to the looper's MIDI input
|
|
||||||
let our_port = "sendmidi:midi_out";
|
|
||||||
let target_port = "looper:midi_in";
|
|
||||||
|
|
||||||
match active_client.as_client().connect_ports_by_name(our_port, target_port) {
|
|
||||||
Ok(_) => println!("✓ Auto-connected to looper:midi_in"),
|
|
||||||
Err(e) => println!("⚠ Could not auto-connect to looper:midi_in: {}\n Make sure the looper is running, then connect manually.", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_expression_value(prompt: &str) -> Result<u8, Box<dyn std::error::Error>> {
|
fn get_expression_value(prompt: &str) -> Result<u8, Box<dyn std::error::Error>> {
|
||||||
print!("{}", prompt);
|
print!("{}", prompt);
|
||||||
io::stdout().flush()?;
|
io::stdout().flush()?;
|
||||||
|
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
io::stdin().read_line(&mut input)?;
|
io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
let value: u8 = input.trim().parse()
|
let value: u8 = input
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
.map_err(|_| "Invalid number. Please enter 0-127")?;
|
.map_err(|_| "Invalid number. Please enter 0-127")?;
|
||||||
|
|
||||||
if value > 127 {
|
if value > 127 {
|
||||||
return Err("Value must be 0-127".into());
|
return Err("Value must be 0-127".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Starting FCB1010 MIDI Simulator...");
|
println!("Starting FCB1010 MIDI Simulator...");
|
||||||
|
|
||||||
// Create JACK client
|
// Create JACK client
|
||||||
let (client, status) = jack::Client::new("sendmidi", jack::ClientOptions::NO_START_SERVER)?;
|
let (client, status) = jack::Client::new("sendmidi", jack::ClientOptions::NO_START_SERVER)?;
|
||||||
if !status.is_empty() {
|
if !status.is_empty() {
|
||||||
@ -107,17 +104,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create command channel
|
// Create command channel
|
||||||
let (command_sender, command_receiver): (Sender<MidiCommand>, Receiver<MidiCommand>) = kanal::bounded(32);
|
let (command_sender, command_receiver): (Sender<MidiCommand>, Receiver<MidiCommand>) =
|
||||||
|
kanal::bounded(32);
|
||||||
|
|
||||||
// Create MIDI sender
|
// Create MIDI sender
|
||||||
let midi_sender = MidiSender::new(&client, command_receiver)?;
|
let midi_sender = MidiSender::new(&client, command_receiver)?;
|
||||||
|
|
||||||
// Activate client
|
// Activate client
|
||||||
let active_client = client.activate_async((), midi_sender)?;
|
let _active_client = client.activate_async((), midi_sender)?;
|
||||||
|
|
||||||
// Auto-connect after a brief delay to let JACK settle
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
auto_connect(&active_client);
|
|
||||||
|
|
||||||
show_help();
|
show_help();
|
||||||
|
|
||||||
@ -135,7 +129,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let first_char = input.chars().next().unwrap();
|
let first_char = input.chars().next().unwrap();
|
||||||
|
|
||||||
let command = match first_char {
|
let command = match first_char {
|
||||||
'1' => Some(MidiCommand::ControlChange { cc: 20, value: 127 }),
|
'1' => Some(MidiCommand::ControlChange { cc: 20, value: 127 }),
|
||||||
'2' => Some(MidiCommand::ControlChange { cc: 21, value: 127 }),
|
'2' => Some(MidiCommand::ControlChange { cc: 21, value: 127 }),
|
||||||
@ -149,33 +143,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
'0' => Some(MidiCommand::ControlChange { cc: 29, value: 127 }),
|
'0' => Some(MidiCommand::ControlChange { cc: 29, value: 127 }),
|
||||||
'u' => Some(MidiCommand::ControlChange { cc: 30, value: 127 }),
|
'u' => Some(MidiCommand::ControlChange { cc: 30, value: 127 }),
|
||||||
'd' => Some(MidiCommand::ControlChange { cc: 31, value: 127 }),
|
'd' => Some(MidiCommand::ControlChange { cc: 31, value: 127 }),
|
||||||
'e' => {
|
'e' => match get_expression_value("Expression A value (0-127): ") {
|
||||||
match get_expression_value("Expression A value (0-127): ") {
|
Ok(value) => Some(MidiCommand::ControlChange { cc: 1, value }),
|
||||||
Ok(value) => Some(MidiCommand::ControlChange { cc: 1, value }),
|
Err(e) => {
|
||||||
Err(e) => {
|
eprintln!("Error: {}", e);
|
||||||
eprintln!("Error: {}", e);
|
None
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'm' => {
|
'm' => match get_expression_value("Master volume value (0-127): ") {
|
||||||
match get_expression_value("Master volume value (0-127): ") {
|
Ok(value) => Some(MidiCommand::ControlChange { cc: 7, value }),
|
||||||
Ok(value) => Some(MidiCommand::ControlChange { cc: 7, value }),
|
Err(e) => {
|
||||||
Err(e) => {
|
eprintln!("Error: {}", e);
|
||||||
eprintln!("Error: {}", e);
|
None
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'h' => {
|
'h' => {
|
||||||
show_help();
|
show_help();
|
||||||
None
|
None
|
||||||
},
|
}
|
||||||
'q' => {
|
'q' => {
|
||||||
println!("Exiting...");
|
println!("Exiting...");
|
||||||
command_sender.send(MidiCommand::Quit)?;
|
command_sender.send(MidiCommand::Quit)?;
|
||||||
break;
|
break;
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Unknown command '{}'. Press 'h' for help.", first_char);
|
println!("Unknown command '{}'. Press 'h' for help.", first_char);
|
||||||
None
|
None
|
||||||
@ -191,4 +181,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/state.rs
Normal file
25
src/state.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct State {
|
||||||
|
pub connections: ConnectionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConnectionState {
|
||||||
|
pub midi_in: Vec<String>,
|
||||||
|
pub audio_in: Vec<String>,
|
||||||
|
pub audio_out: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
connections: ConnectionState {
|
||||||
|
midi_in: Vec::new(),
|
||||||
|
audio_in: Vec::new(),
|
||||||
|
audio_out: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/state_manager.rs
Normal file
131
src/state_manager.rs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
use crate::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::sync::{broadcast, watch};
|
||||||
|
|
||||||
|
pub struct StateManager {
|
||||||
|
state: State,
|
||||||
|
state_tx: watch::Sender<State>,
|
||||||
|
notification_rx: broadcast::Receiver<JackNotification>,
|
||||||
|
state_file_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateManager {
|
||||||
|
pub fn new(
|
||||||
|
notification_rx: broadcast::Receiver<JackNotification>,
|
||||||
|
) -> (Self, watch::Receiver<State>) {
|
||||||
|
let state_file_path = Self::get_state_file_path();
|
||||||
|
let initial_state = Self::load_from_disk(&state_file_path).unwrap_or_default();
|
||||||
|
let (state_tx, state_rx) = watch::channel(initial_state.clone());
|
||||||
|
|
||||||
|
let manager = Self {
|
||||||
|
state: initial_state,
|
||||||
|
state_tx,
|
||||||
|
notification_rx,
|
||||||
|
state_file_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
(manager, state_rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
while let Ok(notification) = self.notification_rx.recv().await {
|
||||||
|
if let JackNotification::PortConnect {
|
||||||
|
port_a,
|
||||||
|
port_b,
|
||||||
|
connected,
|
||||||
|
} = notification
|
||||||
|
{
|
||||||
|
self.handle_port_connection(port_a, port_b, connected)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_port_connection(
|
||||||
|
&mut self,
|
||||||
|
port_a: String,
|
||||||
|
port_b: String,
|
||||||
|
connected: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Determine which port is ours and which is external
|
||||||
|
let (our_port, external_port) = if port_a.starts_with("looper:") {
|
||||||
|
(port_a, port_b)
|
||||||
|
} else if port_b.starts_with("looper:") {
|
||||||
|
(port_b, port_a)
|
||||||
|
} else {
|
||||||
|
// Neither port is ours, ignore
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the connections state
|
||||||
|
let our_port_name = our_port.strip_prefix("looper:").unwrap_or(&our_port);
|
||||||
|
let connections = &mut self.state.connections;
|
||||||
|
|
||||||
|
let port_list = match our_port_name {
|
||||||
|
"midi_in" => &mut connections.midi_in,
|
||||||
|
"audio_in" => &mut connections.audio_in,
|
||||||
|
"audio_out" => &mut connections.audio_out,
|
||||||
|
_ => {
|
||||||
|
log::warn!("Unknown port: {}", our_port_name);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if connected {
|
||||||
|
// Add connection if not already present
|
||||||
|
if !port_list.contains(&external_port) {
|
||||||
|
port_list.push(external_port.clone());
|
||||||
|
log::info!("Added connection: {} -> {}", our_port, external_port);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove connection
|
||||||
|
port_list.retain(|p| p != &external_port);
|
||||||
|
log::info!("Removed connection: {} -> {}", our_port, external_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast state change
|
||||||
|
if let Err(e) = self.state_tx.send(self.state.clone()) {
|
||||||
|
log::error!("Failed to broadcast state change: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
self.save_to_disk()?;
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
let state: State = serde_json::from_str(&content)
|
||||||
|
.map_err(|_| LooperError::StateLoad(std::panic::Location::caller()))?;
|
||||||
|
log::info!("Loaded state from {:?}", path);
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::info!("Could not load state from {:?}. Using defaults.", path);
|
||||||
|
Err(LooperError::StateLoad(std::panic::Location::caller()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_to_disk(&self) -> Result<()> {
|
||||||
|
let json = serde_json::to_string_pretty(&self.state)
|
||||||
|
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
|
std::fs::write(&self.state_file_path, json)
|
||||||
|
.map_err(|_| LooperError::StateSave(std::panic::Location::caller()))?;
|
||||||
|
|
||||||
|
log::debug!("Saved state to {:?}", self.state_file_path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,4 +54,4 @@ impl Track {
|
|||||||
pub fn set_state(&mut self, state: TrackState) {
|
pub fn set_state(&mut self, state: TrackState) {
|
||||||
self.state = state;
|
self.state = state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user