Save and restore connections

This commit is contained in:
2025-06-05 22:58:05 +02:00
parent 7eec4f0093
commit b296b752b3
15 changed files with 503 additions and 104 deletions

View File

@@ -5,13 +5,11 @@ pub struct 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);
// Pre-fill the channel with initial buffers
while let Ok(true) = allocated_buffer_sender
.try_send(AudioChunk::allocate(buffer_size))
{}
while let Ok(true) = allocated_buffer_sender.try_send(AudioChunk::allocate(buffer_size)) {}
let allocator = Allocator {
channel: allocated_buffer_receiver,
@@ -20,7 +18,7 @@ impl Allocator {
let allocated_buffer_sender = allocated_buffer_sender.to_async();
// 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 {
let chunk = AudioChunk::allocate(buffer_size);
if allocated_buffer_sender.send(chunk).await.is_err() {
@@ -29,7 +27,7 @@ impl Allocator {
}
});
(allocator, join_handle)
allocator
}
}

View File

@@ -553,7 +553,7 @@ mod tests {
assert!(result.is_ok());
assert_eq!(chunk.sample_count, 3); // First chunk unchanged
let chunk2 = chunk.next.as_ref().unwrap();
assert_eq!(chunk2.sample_count, 2);
assert_eq!(chunk2.samples[0], 4.0);
@@ -587,7 +587,7 @@ mod tests {
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
assert!(result.is_ok());
// Navigate to chunk3 and verify the sample was added
let chunk2 = chunk1.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 factory = || panic!("Factory should not be called");
// First append
let result1 = AudioChunk::append_samples(&mut chunk, &[1.0, 2.0], &mut factory);
assert!(result1.is_ok());
@@ -644,12 +644,12 @@ mod tests {
let result = AudioChunk::append_samples(&mut chunk1, &samples, &mut factory);
assert!(result.is_ok());
// Check that chunk2 got filled
let chunk2 = chunk1.next.as_ref().unwrap();
assert_eq!(chunk2.sample_count, 3);
assert_eq!(chunk2.samples[2], 6.0);
// Check that chunk3 was created with remaining samples
let chunk3 = chunk2.next.as_ref().unwrap();
assert_eq!(chunk3.sample_count, 2);

View File

@@ -1,10 +1,13 @@
mod allocator;
mod audio_chunk;
mod chunk_factory;
mod connection_manager;
mod looper_error;
mod midi;
mod notification_handler;
mod process_handler;
mod state;
mod state_manager;
mod track;
use std::sync::Arc;
@@ -12,54 +15,76 @@ use std::sync::Arc;
use allocator::Allocator;
use audio_chunk::AudioChunk;
use chunk_factory::ChunkFactory;
use connection_manager::ConnectionManager;
use looper_error::LooperError;
use looper_error::Result;
use notification_handler::JackNotification;
use notification_handler::NotificationHandler;
use process_handler::ProcessHandler;
use state::State;
use state_manager::StateManager;
use track::Track;
use track::TrackState;
pub struct JackPorts {
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>,
}
#[tokio::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 (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)
.expect("Could not create process handler");
let process_handler =
ProcessHandler::new(ports, allocator).expect("Could not create process handler");
let notification_handler = NotificationHandler::new();
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)
.expect("Could not activate Jack");
tokio::select! {
notification = notification_channel.recv() => {
match notification {
Ok(JackNotification::Shutdown {reason, status}) => {
loop {
tokio::select! {
notification = notification_channel.recv() => {
if let Ok(JackNotification::Shutdown {reason, status}) = notification {
log::error!("Jack shutdown: {reason} {status:?}");
}
Err(e) => {
log::error!("NotificationHandler stopped: {e}");
break;
}
}
}
_ = factory_handle => {
log::error!("Allocator was dropped");
}
_ = tokio::signal::ctrl_c() => {
log::info!("Stopping");
result = state_manager.run() => {
if let Err(e) = result {
log::error!("StateManager task failed: {}", e);
}
break;
}
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
.register_port("midi_in", jack::MidiIn::default())
.expect("Could not create midi_in port");
let ports = JackPorts {
audio_in,
audio_out,
midi_in,
};
(jack_client, ports)
}
}

80
src/connection_manager.rs Normal file
View 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),
}
}
}
}

View File

@@ -8,6 +8,15 @@ pub enum LooperError {
#[error("Index out of bounds")]
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>;

View File

@@ -10,7 +10,7 @@ pub fn process_events<F: ChunkFactory>(
const MAX_EVENTS: usize = 16; // Reasonable limit for real-time processing
let mut raw_events = [[0u8; 3]; MAX_EVENTS];
let mut event_count = 0;
// Collect events from the MIDI input iterator
let midi_input = process_handler.ports.midi_in.iter(ps);
for midi_event in midi_input {
@@ -23,12 +23,12 @@ pub fn process_events<F: ChunkFactory>(
return Err(LooperError::OutOfBounds(std::panic::Location::caller()));
}
}
// Now process the collected events using wmidi
// The iterator borrow is dropped, so we can mutably borrow process_handler
for i in 0..event_count {
let event_bytes = &raw_events[i];
// Use wmidi for cleaner MIDI parsing instead of manual byte checking
match wmidi::MidiMessage::try_from(&event_bytes[..]) {
Ok(message) => {
@@ -62,6 +62,6 @@ pub fn process_events<F: ChunkFactory>(
}
}
}
Ok(())
}
}

View File

@@ -4,18 +4,24 @@ pub struct NotificationHandler {
#[derive(Clone, Debug)]
pub enum JackNotification {
Shutdown{
Shutdown {
status: jack::ClientStatus,
reason: String
reason: String,
},
PortConnect {
port_a: String,
port_b: String,
connected: bool,
},
PortRegistered {
port_name: String,
},
}
impl NotificationHandler {
pub fn new() -> Self {
let (channel, _) = tokio::sync::broadcast::channel(1);
Self {
channel,
}
let (channel, _) = tokio::sync::broadcast::channel(16);
Self { channel }
}
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<JackNotification> {
@@ -26,6 +32,52 @@ impl NotificationHandler {
impl jack::NotificationHandler for NotificationHandler {
unsafe fn shutdown(&mut self, status: jack::ClientStatus, reason: &str) {
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");
};
}
}

View File

@@ -8,10 +8,7 @@ pub struct ProcessHandler<F: ChunkFactory> {
}
impl<F: ChunkFactory> ProcessHandler<F> {
pub fn new(
ports: JackPorts,
mut chunk_factory: F,
) -> Result<Self> {
pub fn new(ports: JackPorts, mut chunk_factory: F) -> Result<Self> {
Ok(Self {
track: Track::new(&mut chunk_factory)?,
playback_position: 0,
@@ -63,7 +60,7 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
if midi::process_events(self, ps).is_err() {
return jack::Control::Quit;
}
let input_buffer = self.ports.audio_in.as_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
}
}
}

View File

@@ -1,6 +1,6 @@
use std::io::{self, Write};
use jack::RawMidi;
use kanal::{Receiver, Sender};
use std::io::{self, Write};
#[derive(Debug)]
enum MidiCommand {
@@ -14,7 +14,10 @@ struct 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
.register_port("midi_out", jack::MidiOut::default())
.map_err(|e| format!("Could not create MIDI output port: {}", e))?;
@@ -29,20 +32,23 @@ impl MidiSender {
impl jack::ProcessHandler for MidiSender {
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
let mut midi_writer = self.midi_out.writer(ps);
// Process all pending commands
while let Ok(Some(command)) = self.command_receiver.try_recv() {
match command {
MidiCommand::ControlChange { cc, value } => {
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);
}
}
MidiCommand::Quit => return jack::Control::Quit,
}
}
jack::Control::Continue
}
}
@@ -69,37 +75,28 @@ fn show_help() {
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>> {
print!("{}", prompt);
io::stdout().flush()?;
let mut input = String::new();
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")?;
if value > 127 {
return Err("Value must be 0-127".into());
}
Ok(value)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Starting FCB1010 MIDI Simulator...");
// Create JACK client
let (client, status) = jack::Client::new("sendmidi", jack::ClientOptions::NO_START_SERVER)?;
if !status.is_empty() {
@@ -107,17 +104,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
// 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
let midi_sender = MidiSender::new(&client, command_receiver)?;
// Activate client
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);
let _active_client = client.activate_async((), midi_sender)?;
show_help();
@@ -135,7 +129,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
let first_char = input.chars().next().unwrap();
let command = match first_char {
'1' => Some(MidiCommand::ControlChange { cc: 20, 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 }),
'u' => Some(MidiCommand::ControlChange { cc: 30, value: 127 }),
'd' => Some(MidiCommand::ControlChange { cc: 31, value: 127 }),
'e' => {
match get_expression_value("Expression A value (0-127): ") {
Ok(value) => Some(MidiCommand::ControlChange { cc: 1, value }),
Err(e) => {
eprintln!("Error: {}", e);
None
}
'e' => match get_expression_value("Expression A value (0-127): ") {
Ok(value) => Some(MidiCommand::ControlChange { cc: 1, value }),
Err(e) => {
eprintln!("Error: {}", e);
None
}
},
'm' => {
match get_expression_value("Master volume value (0-127): ") {
Ok(value) => Some(MidiCommand::ControlChange { cc: 7, value }),
Err(e) => {
eprintln!("Error: {}", e);
None
}
'm' => match get_expression_value("Master volume value (0-127): ") {
Ok(value) => Some(MidiCommand::ControlChange { cc: 7, value }),
Err(e) => {
eprintln!("Error: {}", e);
None
}
},
'h' => {
show_help();
None
},
}
'q' => {
println!("Exiting...");
command_sender.send(MidiCommand::Quit)?;
break;
},
}
_ => {
println!("Unknown command '{}'. Press 'h' for help.", first_char);
None
@@ -191,4 +181,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
Ok(())
}
}

25
src/state.rs Normal file
View 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
View 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(())
}
}

View File

@@ -54,4 +54,4 @@ impl Track {
pub fn set_state(&mut self, state: TrackState) {
self.state = state;
}
}
}