diff --git a/src/audio.rs b/src/audio.rs index 8021d42..996af5b 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,5 +1,6 @@ -use jack::{AudioIn, AudioOut, ClientOptions}; +use jack::{AudioIn, AudioOut, ClientOptions, NotificationHandler}; use oximedia_metering::vu_meter::VuMeter; +use serde::{Deserialize, Serialize}; use tokio::sync::*; #[derive(Debug)] @@ -38,10 +39,70 @@ pub struct TtsOutStream { pub sample_rate: u32 } +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct AudioConfig { + mic_in_connections: Vec, + tts_out_connections: Vec +} + +impl AudioConfig { + pub fn load() -> Self { + if let Ok(contents) = std::fs::read_to_string("audio.json") { + serde_json::from_str(contents.as_str()).unwrap() + } else { + Default::default() + } + } +} + +#[derive(Debug)] +struct Notify { + config: AudioConfig, + mic_port: jack::Port, + tts_port: jack::Port, + log: mpsc::Sender +} + +impl NotificationHandler for Notify { + fn ports_connected( + &mut self, + client: &jack::Client, + port_id_a: jack::PortId, + port_id_b: jack::PortId, + are_connected: bool, + ) { + let port_a = client.port_by_id(port_id_a).unwrap(); + let port_b = client.port_by_id(port_id_b).unwrap(); + + let (stream_name, other_port, target_cfg) = if port_b == self.mic_port { + ("Microphone input", port_a, &mut self.config.mic_in_connections) + } else if port_a == self.tts_port { + ("TTS output", port_b, &mut self.config.tts_out_connections) + } else { + return; + }; + + if let Ok(port_name) = other_port.name() { + if are_connected { + self.log.blocking_send(format!("{} connected to {}", stream_name, port_name)).unwrap(); + target_cfg.push(port_name); + } else { + self.log.blocking_send(format!("{} disconnected from {}", stream_name, port_name)).unwrap(); + target_cfg.retain(|x| { x != &port_name} ); + } + + let save_data = serde_json::to_string_pretty(&self.config).unwrap(); + std::fs::write("audio.json", save_data).unwrap(); + } + } +} + pub async fn start_audio_input(messages: &mpsc::Sender) -> (AudioInputControl, MicStream, TtsOutStream) { let (exit_tx, exit_rx) = oneshot::channel(); + let config = AudioConfig::load(); + let (mic_audio_sink, mic_audio_src) = mpsc::channel(32); let (tts_audio_sink, mut tts_audio_src) = mpsc::channel(32); let (volume_sink, volume_src) = watch::channel(0.); @@ -51,12 +112,32 @@ pub async fn start_audio_input(messages: &mpsc::Sender) -> (AudioInputCo let mut tts_port = client.register_port("tts-out", AudioOut::default()).unwrap(); let rate = client.sample_rate(); - if let Ok(_) = client.connect_ports_by_name("mixxx-mic-1:capture_MONO", mic_port.name().unwrap().as_str()) { - messages.send("Connected to audio.".into()).await.unwrap(); - } else { - messages.send("Failed to reconnect to audio.".into()).await.unwrap(); + let mic_name = mic_port.name().unwrap(); + let tts_name = tts_port.name().unwrap(); + + for port in &config.mic_in_connections { + if let Ok(_) = client.connect_ports_by_name(&port, &mic_name) { + messages.send(format!("Connected mic to {}", port)).await.unwrap(); + } else { + messages.send(format!("Failed to reconnect mic to {}.", port)).await.unwrap(); + } } + for port in &config.tts_out_connections { + if let Ok(_) = client.connect_ports_by_name(&tts_name, &port) { + messages.send(format!("Connected TTS output to {}", port)).await.unwrap(); + } else { + messages.send(format!("Failed to reconnect TTS output to {}.", port)).await.unwrap(); + } + } + + let notifier = Notify { + config, + mic_port: mic_port.clone_unowned(), + tts_port: tts_port.clone_unowned(), + log: messages.clone() + }; + let mut meter = VuMeter::new(rate.into(), 1, None); let mut tts_output_buf = vec![]; tts_output_buf.reserve(1024); @@ -78,7 +159,6 @@ pub async fn start_audio_input(messages: &mpsc::Sender) -> (AudioInputCo }); } - if let Ok(mut next_outbuf) = tts_audio_src.try_recv() { tts_output_buf.append(&mut next_outbuf); } @@ -100,7 +180,7 @@ pub async fn start_audio_input(messages: &mpsc::Sender) -> (AudioInputCo }); tokio::spawn(async move { - let async_client = client.activate_async((), handler).unwrap(); + let async_client = client.activate_async(notifier, handler).unwrap(); exit_rx.await.unwrap(); diff --git a/src/main.rs b/src/main.rs index 789e9ce..bc98de0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,6 @@ mod tts; mod prediction; mod audio; -// TODO: We should have a separate 'state.json' file, which remembers jack connections, and the world time for the show to end. Then we only update the 'time remaining' field in the scene and only deal with relative durations inside the scene data // TODO: We should be able to delete entries from the conversation, or at least go back and edit something I said. // TODO: I want a "mark" command or keyboard shortcut, that inserts a marker into the log, so I know where to come back for the next speaking segment. // TODO: If we insert text without speaking, this should be indicated visually somehow