Implement room connection, handle local and remote audio streams, refactor client state management #40
Labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
puregarlic/microclimate!40
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue/3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This is the big PR that makes the whole program work. First, it restructures the client application to use multiple managed values instead of a single
AppStatestruct. It streamlines configuration management by delegating it to aConfigManager, which enables other managers to react to value changes. This PR further allows the Tauri client to connect to a microclimate channel, backed by a LiveKit room. Finally, it enables the client to send microphone data to other participants in the room, and handles sending remote audio tracks to a local playback device.There was quite a bit of LLM involved in this one, mostly due to complexity introduced by the desire to retain the
daspcrate'sSignalAPI, which does not implementSend, and so was problematic for asynchronous tasks.Also of note is the specified target directory in
.cargo/config.toml. After adding thelivekitcrate to the Tauri project, I encountered issues with paths that were longer than Windows' supported 255 character-length paths. I had to shorten the target directory path as much as possible, and it resolved my issue. Any other attempted solutions would be welcome, though, because I don't really like this one all that much.I have tested and validated that audio publishing and subscription works on my local machine, with two separate instances of the client application. Let me know if you want a working
.envwith LiveKit credentials you can use to test.Closes #3
Closes #4
Closes #5
Closes #37
Closes #41
@ -32,0 +38,4 @@/// `Sized`, so all dasp adapters (`.map()`, `.scale_amp()`, rate/// converters, etc.) work on it directly.#[instrument(skip(self))]pub async fn get_signal(&self) -> Result<(BoxedSignal<i16>, u32), AudioDeviceError> {just from personal experience, if there is any chance at all of a tuple type ever changing/getting longer in the future you should give it a type name
@ -127,3 +131,4 @@sample_rate,})}I don't like getters in rust
@ -0,0 +31,4 @@}// SAFETY: The inner Box holds a `Send` trait object.unsafe impl<F> Send for BoxedSignal<F> {}compiles without the unsafe impl. I'm not sure this does anything. Signal implements send with the
+ Sendinvocation in the typedef, which means the Box and therefore the BoxedSignal should also implement Send.@ -0,0 +85,4 @@}// SAFETY: Receiver<Vec<f32>> is Send.unsafe impl Send for ReceiverSignal {}same as above
@ -0,0 +45,4 @@impl ChannelsManager {#[instrument(name = "channels_manager_new", skip(config))]pub fn new(config: ConfigManager) -> Arc<Self> {let manager = Arc::new(ChannelsManager {no reason to make a variable here -- the whole function can just be
@ -0,0 +181,4 @@let room_arc = self.room.clone();let output_rate = output.lock().await.sample_rate();let output_for_events = output.clone();let events_handle = tauri::async_runtime::spawn(async move {It's sort of annoying, but I'm a fan of doing functions for the bodies of async_runtime::spawn if possible, to avoid tabbing over too much. ymmv
@ -0,0 +235,4 @@// Spawn the mic capture pipeline:// blocking thread → reads dasp signal, batches into 10 ms frames// async task → receives frames, calls source.capture_frame()let samples_per_channel = (sample_rate / 100) as usize; // 10 msavoid magic numbers imo
@ -0,0 +1,19 @@use tracing::instrument;wrt my comment above, I think it'd make sense to merge this into the config manager or find some other non-mod.rs based pattern
@ -0,0 +1,53 @@use std::sync::Arc;we should figure out how we feel about this early.
https://users.rust-lang.org/t/module-mod-rs-or-module-rs/122653
I prefer either not having mod.rs at all (calling it i.e. config_manager.rs and removing the config/ folder altogether) or only using mod.rs for reexports. there are other examples of projects that like having mod.rs contain logic, like axum. even then, they seem a little inconsistent about it.
another option is config/config_manager.rs
the main advantage of naming the file is that you don't have a bunch of files called mod.rs all over the project. I could see it either way though
@ -22,1 +28,4 @@.setup(|app| {setup_tracing();let _guard = info_span!("setting up managers").entered();you can just do
let _ = ...@ -1,17 +1,15 @@use std::sync::Arc;this file and state.rs are no longer included in the project and can be deleted, assuming they're supposed to be subsumed by the new managers
2c87d31640to68bbaba645@ -79,0 +25,4 @@}// Start the input stream and obtain a live i16 signal.let crate::audio::input::CapturedAudio {go fish
@ -0,0 +38,4 @@/// construction — `dasp::signal::from_iter` calls `iter.next()` immediately on/// construction, which would block for a blocking iterator.pub struct ReceiverSignal {rx: mpsc::Receiver<Vec<f32>>,go fish
@ -0,0 +44,4 @@}impl ReceiverSignal {pub fn new(rx: mpsc::Receiver<Vec<f32>>) -> Self {go fish
@ -0,0 +68,4 @@self.current = batch.into_iter();self.current.next().unwrap_or(0.0)}Err(mpsc::TryRecvError::Empty) => 0.0,go fish
@ -0,0 +36,4 @@audio: State<'_, AudioManager>,channel_id: String,) -> Result<(), String> {let crate::audio::input::CapturedAudio {go fish
@ -0,0 +75,4 @@let manager = Arc::clone(&self);let config = self.config.clone();tauri::async_runtime::spawn(handle_config_changes(config, manager));go fish
@ -0,0 +192,4 @@}// Spawn the room-events task with track subscription handling.let events_handle = tauri::async_runtime::spawn(handle_room_events(go fish
@ -0,0 +211,4 @@// async task → receives frames, calls source.capture_frame()let samples_per_channel = (sample_rate / (1000 / FRAME_DURATION_MS)) as usize;let (frame_tx, mut frame_rx) =tokio::sync::mpsc::channel::<Vec<i16>>(MIC_FRAME_CHANNEL_CAPACITY);go fish
@ -0,0 +220,4 @@buf.push(signal.next());if buf.len() >= samples_per_channel {let batch =std::mem::replace(&mut buf, Vec::with_capacity(samples_per_channel));go fish
@ -0,0 +340,4 @@output: &Arc<Mutex<OutputDeviceManager>>,) -> Option<(JoinHandle<()>, StreamHandle)> {let mut stream = NativeAudioStream::new(track.rtc_track(), sample_rate as i32, MONO_CHANNEL_COUNT as i32);let (tx, rx) = std::sync::mpsc::channel::<Vec<f32>>();go fish