diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..6a941bf --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,88 @@ +use jack::{AudioIn, ClientOptions}; +use oximedia_metering::vu_meter::VuMeter; +use tokio::sync::*; + +#[derive(Debug)] +pub struct JackClientRef { + killswitch: Option> +} + +impl Drop for JackClientRef { + fn drop(&mut self) { + self.killswitch.take().unwrap().send(()).unwrap(); + } +} + +#[derive(Debug)] +pub struct AudioInputControl { + volume_src: watch::Receiver, + _jack_client: JackClientRef +} + +impl AudioInputControl { + pub async fn next(&mut self) -> f64 { + self.volume_src.changed().await.unwrap(); + *self.volume_src.borrow_and_update() + } +} + +#[derive(Debug)] +pub struct MicStream { + pub src: mpsc::Receiver>, + pub sample_rate: u32 +} + +pub async fn start_audio_input(messages: &mpsc::Sender) -> (AudioInputControl, MicStream) { + + let (exit_tx, exit_rx) = oneshot::channel(); + + let (mic_audio_sink, mic_audio_src) = mpsc::channel(32); + let (volume_sink, volume_src) = watch::channel(0.); + + let (client, _status) = jack::Client::new("Eva-Cohost", ClientOptions::default() | ClientOptions::SESSION_ID).unwrap(); + let mic_port = client.register_port("microphone-in", AudioIn::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 mut meter = VuMeter::new(rate.into(), 1, None); + + let handler = jack::contrib::ClosureProcessHandler::new(move |_client, scope| { + if mic_port.connected_count().unwrap() > 0 { + let buf: Vec<_> = mic_port.as_slice(scope).iter().copied().collect(); + meter.process_interleaved(&buf); + mic_audio_sink.blocking_send(buf).unwrap(); + + volume_sink.send_if_modified(|v| { + let next_vu = meter.channel_vu(0).unwrap(); + if *v != next_vu { + *v = next_vu; + true + } else { + false + } + }); + } + jack::Control::Continue + }); + + tokio::spawn(async move { + let async_client = client.activate_async((), handler).unwrap(); + + exit_rx.await.unwrap(); + + async_client.deactivate().unwrap(); + }); + + (AudioInputControl { + volume_src, + _jack_client: JackClientRef { killswitch: Some(exit_tx) } + }, MicStream { + sample_rate: rate, + src: mic_audio_src + }) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1977949..b818336 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,13 +13,14 @@ use futures::{StreamExt, future::FutureExt}; use ratatui::prelude::*; use tui_skeleton::{AnimationMode, SkeletonText}; -use crate::{prediction::{BandcampResult, PossibleResponse}, scene::{ConversationEntry, Scene, StageActions, StageDirection}, transcription::{AudioInputControl, TranscriptionControl, start_audio_input}, tts::{TtsControl, start_tts}}; +use crate::{audio::{AudioInputControl, start_audio_input}, prediction::{BandcampResult, PossibleResponse}, scene::{ConversationEntry, Scene, StageActions, StageDirection}, transcription::TranscriptionControl, tts::{TtsControl, start_tts}}; mod scene; mod events; mod transcription; 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. @@ -643,7 +644,7 @@ async fn main() { transcription_result = app.transcription.next() => { app.next_actions.push(ConversationEntry::User(transcription_result)); app.regenerate_responses(); - } + }, maybe_event = event => { match maybe_event { Some(Ok(event)) => { diff --git a/src/transcription.rs b/src/transcription.rs index eab4aea..a6a7c79 100644 --- a/src/transcription.rs +++ b/src/transcription.rs @@ -1,12 +1,10 @@ use std::{io::Read, sync::{Arc, Mutex}}; use async_openai::{Client, config::OpenAIConfig, types::{InputSource, audio::{AudioInput, CreateTranscriptionRequest}}}; -use jack::{AudioIn, ClientOptions}; -use oximedia_metering::vu_meter::VuMeter; use tempfile::SpooledData; -use tokio::sync::{mpsc, oneshot, watch}; +use tokio::sync::{mpsc, watch}; -use crate::events::AudioRecordRequest; +use crate::{audio::MicStream, events::AudioRecordRequest}; #[derive(Debug)] pub struct TranscriptionControl { @@ -28,36 +26,6 @@ impl TranscriptionControl { } } -#[derive(Debug)] -pub struct JackClientRef { - killswitch: Option> -} - -impl Drop for JackClientRef { - fn drop(&mut self) { - self.killswitch.take().unwrap().send(()).unwrap(); - } -} - -#[derive(Debug)] -pub struct AudioInputControl { - volume_src: watch::Receiver, - _jack_client: JackClientRef -} - -impl AudioInputControl { - pub async fn next(&mut self) -> f64 { - self.volume_src.changed().await.unwrap(); - *self.volume_src.borrow_and_update() - } -} - -#[derive(Debug)] -pub struct MicStream { - src: mpsc::Receiver>, - sample_rate: u32 -} - struct RcFile(Arc>); impl std::io::Write for RcFile { @@ -150,59 +118,4 @@ pub async fn start_transcription(mut mic_src: MicStream) -> TranscriptionControl }); ret -} - -pub async fn start_audio_input(messages: &mpsc::Sender) -> (AudioInputControl, MicStream) { - - let (exit_tx, exit_rx) = oneshot::channel(); - - let (mic_audio_sink, mic_audio_src) = mpsc::channel(32); - let (volume_sink, volume_src) = watch::channel(0.); - - let (client, _status) = jack::Client::new("Eva-Cohost", ClientOptions::default() | ClientOptions::SESSION_ID).unwrap(); - let mic_port = client.register_port("microphone-in", AudioIn::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 mut meter = VuMeter::new(rate.into(), 1, None); - - let handler = jack::contrib::ClosureProcessHandler::new(move |_client, scope| { - if mic_port.connected_count().unwrap() > 0 { - let buf: Vec<_> = mic_port.as_slice(scope).iter().copied().collect(); - meter.process_interleaved(&buf); - mic_audio_sink.blocking_send(buf).unwrap(); - - volume_sink.send_if_modified(|v| { - let next_vu = meter.channel_vu(0).unwrap(); - if *v != next_vu { - *v = next_vu; - true - } else { - false - } - }); - } - jack::Control::Continue - }); - - tokio::spawn(async move { - let async_client = client.activate_async((), handler).unwrap(); - - exit_rx.await.unwrap(); - - async_client.deactivate().unwrap(); - }); - - (AudioInputControl { - volume_src, - _jack_client: JackClientRef { killswitch: Some(exit_tx) } - }, MicStream { - sample_rate: rate, - src: mic_audio_src - }) } \ No newline at end of file