Skip to content

Commit 1ba31e4

Browse files
authored
Refactor audio input feed to use actor (#937)
* audio actor? * blocking mic feed actor * manage connect and error events properly * restructure to be async * cleanuop * formatting * don't hold app state while setting mic input
1 parent 4bc8e51 commit 1ba31e4

File tree

23 files changed

+818
-561
lines changed

23 files changed

+818
-561
lines changed

Cargo.lock

Lines changed: 20 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/cli/src/record.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ impl RecordStart {
6565
cap_recording::RecordingBaseInputs {
6666
capture_target: target_info,
6767
capture_system_audio: self.system_audio,
68-
mic_feed: &None,
68+
mic_feed: None,
6969
},
7070
camera.map(|c| Arc::new(Mutex::new(c))),
7171
false,

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ md5 = "0.7.0"
101101
tokio-util = "0.7.15"
102102
wgpu.workspace = true
103103
bytemuck = "1.23.1"
104+
kameo = "0.17.2"
104105

105106
[target.'cfg(target_os = "macos")'.dependencies]
106107
core-graphics = "0.24.0"

apps/desktop/src-tauri/src/audio_meter.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use cap_recording::feeds::{AudioInputSamples, AudioInputSamplesReceiver};
1+
use cap_recording::feeds::microphone::MicrophoneSamples;
22
use cpal::{SampleFormat, StreamInstant};
33
use keyed_priority_queue::KeyedPriorityQueue;
44
use serde::{Deserialize, Serialize};
@@ -15,7 +15,10 @@ const MIN_DB: f64 = -96.0;
1515
#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
1616
pub struct AudioInputLevelChange(f64);
1717

18-
pub fn spawn_event_emitter(app_handle: AppHandle, audio_input_rx: AudioInputSamplesReceiver) {
18+
pub fn spawn_event_emitter(
19+
app_handle: AppHandle,
20+
audio_input_rx: flume::Receiver<MicrophoneSamples>,
21+
) {
1922
let mut time_window = VolumeMeter::new(0.2);
2023
tokio::spawn(async move {
2124
while let Ok(samples) = audio_input_rx.recv_async().await {
@@ -106,7 +109,7 @@ fn db_fs(data: impl Iterator<Item = f64>) -> f64 {
106109
(20.0 * (max as f64 / MAX_AMPLITUDE_F32).log10()).clamp(MIN_DB, 0.0)
107110
}
108111

109-
fn samples_to_f64(samples: &AudioInputSamples) -> impl Iterator<Item = f64> + use<'_> {
112+
fn samples_to_f64(samples: &MicrophoneSamples) -> impl Iterator<Item = f64> + use<'_> {
110113
samples
111114
.data
112115
.chunks(samples.format.sample_size())

apps/desktop/src-tauri/src/lib.rs

Lines changed: 68 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -27,72 +27,64 @@ use audio::AppSounds;
2727
use auth::{AuthStore, AuthenticationInvalid, Plan};
2828
use camera::{CameraPreview, CameraWindowState};
2929
use cap_displays::{DisplayId, WindowId, bounds::LogicalBounds};
30-
use cap_editor::EditorInstance;
31-
use cap_editor::EditorState;
32-
use cap_project::RecordingMetaInner;
33-
use cap_project::XY;
30+
use cap_editor::{EditorInstance, EditorState};
3431
use cap_project::{
35-
ProjectConfiguration, RecordingMeta, SharingMeta, StudioRecordingMeta, ZoomSegment,
32+
ProjectConfiguration, RecordingMeta, RecordingMetaInner, SharingMeta, StudioRecordingMeta, XY,
33+
ZoomSegment,
3634
};
37-
use cap_recording::feeds::DeviceOrModelID;
3835
use cap_recording::{
39-
feeds::CameraFeed,
40-
feeds::RawCameraFrame,
41-
feeds::{AudioInputFeed, AudioInputSamplesSender},
36+
feeds::{
37+
self, CameraFeed, DeviceOrModelID, RawCameraFrame,
38+
microphone::{self, MicrophoneFeed},
39+
},
4240
sources::ScreenCaptureTarget,
4341
};
44-
use cap_rendering::ProjectRecordingsMeta;
45-
use cap_rendering::RenderedFrame;
42+
use cap_rendering::{ProjectRecordingsMeta, RenderedFrame};
4643
use clipboard_rs::common::RustImage;
4744
use clipboard_rs::{Clipboard, ClipboardContext};
48-
use editor_window::EditorInstances;
49-
use editor_window::WindowEditorInstance;
45+
use editor_window::{EditorInstances, WindowEditorInstance};
5046
use general_settings::GeneralSettingsStore;
47+
use kameo::{Actor, actor::ActorRef};
5148
use mp4::Mp4Reader;
5249
use notifications::NotificationType;
5350
use png::{ColorType, Encoder};
5451
use recording::InProgressRecording;
5552
use relative_path::RelativePathBuf;
56-
57-
use scap::capturer::Capturer;
58-
use scap::frame::Frame;
59-
use scap::frame::VideoFrame;
53+
use scap::{
54+
capturer::Capturer,
55+
frame::{Frame, VideoFrame},
56+
};
6057
use serde::{Deserialize, Serialize};
6158
use serde_json::json;
6259
use specta::Type;
63-
use std::collections::BTreeMap;
64-
use std::path::Path;
65-
use std::time::Duration;
6660
use std::{
61+
collections::BTreeMap,
6762
fs::File,
6863
future::Future,
6964
io::{BufReader, BufWriter},
7065
marker::PhantomData,
71-
path::PathBuf,
66+
path::{Path, PathBuf},
7267
process::Command,
7368
str::FromStr,
7469
sync::Arc,
70+
time::Duration,
7571
};
76-
use tauri::Window;
77-
use tauri::{AppHandle, Manager, State, WindowEvent};
72+
use tauri::{AppHandle, Manager, State, Window, WindowEvent};
7873
use tauri_plugin_deep_link::DeepLinkExt;
7974
use tauri_plugin_dialog::DialogExt;
8075
use tauri_plugin_global_shortcut::GlobalShortcutExt;
8176
use tauri_plugin_notification::{NotificationExt, PermissionState};
8277
use tauri_plugin_opener::OpenerExt;
8378
use tauri_plugin_shell::ShellExt;
8479
use tauri_specta::Event;
85-
use tokio::sync::mpsc;
86-
use tokio::sync::{Mutex, RwLock};
87-
use tokio::time::timeout;
88-
use tracing::debug;
89-
use tracing::error;
90-
use tracing::trace;
80+
use tokio::{
81+
sync::{Mutex, RwLock, mpsc},
82+
time::timeout,
83+
};
84+
use tracing::{error, trace};
9185
use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video};
9286
use web_api::ManagerExt as WebManagerExt;
93-
use windows::EditorWindowIds;
94-
use windows::set_window_transparent;
95-
use windows::{CapWindowId, ShowCapWindow};
87+
use windows::{CapWindowId, EditorWindowIds, ShowCapWindow, set_window_transparent};
9688

9789
use crate::upload::build_video_meta;
9890

@@ -116,15 +108,13 @@ pub struct App {
116108
#[serde(skip)]
117109
camera_feed_initialization: Option<mpsc::Sender<()>>,
118110
#[serde(skip)]
119-
mic_feed: Option<AudioInputFeed>,
120-
#[serde(skip)]
121-
mic_samples_tx: AudioInputSamplesSender,
122-
#[serde(skip)]
123111
handle: AppHandle,
124112
#[serde(skip)]
125113
recording_state: RecordingState,
126114
#[serde(skip)]
127115
recording_logging_handle: LoggingHandle,
116+
#[serde(skip)]
117+
mic_feed: ActorRef<feeds::microphone::MicrophoneFeed>,
128118
server_url: String,
129119
}
130120

@@ -227,27 +217,26 @@ impl App {
227217
#[tauri::command]
228218
#[specta::specta]
229219
async fn set_mic_input(state: MutableState<'_, App>, label: Option<String>) -> Result<(), String> {
230-
let mut app = state.write().await;
220+
let mic_feed = state.read().await.mic_feed.clone();
231221

232-
match (label, &mut app.mic_feed) {
233-
(Some(label), None) => {
234-
AudioInputFeed::init(&label)
235-
.await
236-
.map_err(|e| e.to_string())
237-
.map(async |feed| {
238-
feed.add_sender(app.mic_samples_tx.clone()).await.unwrap();
239-
app.mic_feed = Some(feed);
240-
})
241-
.transpose_async()
222+
match label {
223+
None => {
224+
mic_feed
225+
.ask(microphone::RemoveInput)
242226
.await
227+
.map_err(|e| e.to_string())?;
243228
}
244-
(Some(label), Some(feed)) => feed.switch_input(&label).await.map_err(|e| e.to_string()),
245-
(None, _) => {
246-
debug!("removing mic in set_start_recording_options");
247-
app.mic_feed.take();
248-
Ok(())
229+
Some(label) => {
230+
mic_feed
231+
.ask(feeds::microphone::SetInput { label })
232+
.await
233+
.map_err(|e| e.to_string())?
234+
.await
235+
.map_err(|e| e.to_string())?;
249236
}
250237
}
238+
239+
Ok(())
251240
}
252241

253242
#[tauri::command]
@@ -1119,7 +1108,7 @@ async fn list_audio_devices() -> Result<Vec<String>, ()> {
11191108
return Ok(vec![]);
11201109
}
11211110

1122-
Ok(AudioInputFeed::list_devices().keys().cloned().collect())
1111+
Ok(MicrophoneFeed::list().keys().cloned().collect())
11231112
}
11241113

11251114
#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)]
@@ -2024,7 +2013,28 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
20242013

20252014
let (camera_tx, camera_ws_port, _shutdown) = camera_legacy::create_camera_preview_ws().await;
20262015

2027-
let (audio_input_tx, audio_input_rx) = AudioInputFeed::create_channel();
2016+
let (mic_samples_tx, mic_samples_rx) = flume::bounded(8);
2017+
2018+
let mic_feed = {
2019+
let (error_tx, error_rx) = flume::bounded(1);
2020+
2021+
let mic_feed = MicrophoneFeed::spawn(MicrophoneFeed::new(error_tx));
2022+
2023+
// TODO: make this part of a global actor one day
2024+
tokio::spawn(async move {
2025+
let Ok(err) = error_rx.recv_async().await else {
2026+
return;
2027+
};
2028+
2029+
error!("Mic feed actor error: {err}");
2030+
});
2031+
2032+
let _ = mic_feed
2033+
.ask(feeds::microphone::AddSender(mic_samples_tx))
2034+
.await;
2035+
2036+
mic_feed
2037+
};
20282038

20292039
tauri::async_runtime::set(tokio::runtime::Handle::current());
20302040

@@ -2121,10 +2131,9 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
21212131
handle: app.clone(),
21222132
camera_feed: None,
21232133
camera_feed_initialization: None,
2124-
mic_samples_tx: audio_input_tx,
2125-
mic_feed: None,
21262134
recording_state: RecordingState::None,
21272135
recording_logging_handle,
2136+
mic_feed,
21282137
server_url: GeneralSettingsStore::get(&app)
21292138
.ok()
21302139
.flatten()
@@ -2173,7 +2182,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
21732182
}
21742183
});
21752184

2176-
audio_meter::spawn_event_emitter(app.clone(), audio_input_rx);
2185+
audio_meter::spawn_event_emitter(app.clone(), mic_samples_rx);
21772186

21782187
tray::create_tray(&app).unwrap();
21792188

@@ -2213,7 +2222,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
22132222
let app_state = &mut *state.write().await;
22142223

22152224
if !app_state.is_recording_active_or_pending() {
2216-
app_state.mic_feed.take();
2225+
let _ =
2226+
app_state.mic_feed.ask(microphone::RemoveInput).await;
22172227
app_state.camera_feed.take();
22182228

22192229
if let Some(camera) = CapWindowId::Camera.get(&app) {
@@ -2422,28 +2432,6 @@ trait EventExt: tauri_specta::Event {
24222432

24232433
impl<T: tauri_specta::Event> EventExt for T {}
24242434

2425-
trait TransposeAsync {
2426-
type Output;
2427-
2428-
fn transpose_async(self) -> impl Future<Output = Self::Output>
2429-
where
2430-
Self: Sized;
2431-
}
2432-
2433-
impl<F: Future<Output = T>, T, E> TransposeAsync for Result<F, E> {
2434-
type Output = Result<T, E>;
2435-
2436-
async fn transpose_async(self) -> Self::Output
2437-
where
2438-
Self: Sized,
2439-
{
2440-
match self {
2441-
Ok(f) => Ok(f.await),
2442-
Err(e) => Err(e),
2443-
}
2444-
}
2445-
}
2446-
24472435
fn open_project_from_path(path: &Path, app: AppHandle) -> Result<(), String> {
24482436
let meta = RecordingMeta::load_for_project(path).map_err(|v| v.to_string())?;
24492437

0 commit comments

Comments
 (0)