Skip to content

Conversation

@Brendonovich
Copy link
Contributor

@Brendonovich Brendonovich commented Aug 25, 2025

Converts the audio input feed (now microphone feed) to be a Kameo actor, making the logic of initialisation, selection, and locking much more correct and easy to follow.

Summary by CodeRabbit

  • New Features

    • New actor-based microphone system with device listing, selection, and multi-listener streaming; mic available to instant and studio recordings.
    • Audio level meter updated to consume the new mic stream and shows levels only when a mic is selected.
  • Improvements

    • App can set mic from launch options; mic/camera initialization is more explicit and predictable.
  • Bug Fixes

    • Removed stray debug console logging.
  • Chores

    • Added dependencies and a new internal audio bridge crate to support the updated audio pipeline.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 25, 2025

Walkthrough

Replaces the legacy AudioInputFeed with an actor-based MicrophoneFeed and MicrophoneSamples across desktop and recording crates, updates RecordingBaseInputs to own an Option<Arc>, adds a cap-cpal-ffmpeg crate and related conversions, adjusts UI initialization and mutation API, and updates several signatures and dependencies.

Changes

Cohort / File(s) Summary
Desktop mic actor integration
apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/src/audio_meter.rs, apps/desktop/src-tauri/src/recording.rs, apps/desktop/src-tauri/Cargo.toml
Replaces AudioInputFeed with a kameo ActorRef-based MicrophoneFeed; audio meter now consumes flume::Receiver<MicrophoneSamples>; recording code uses Lock/RemoveInput messages; adds kameo dependency.
Recording crate mic refactor
crates/recording/src/feeds/microphone.rs, crates/recording/src/feeds/mod.rs, crates/recording/src/feeds/audio_input.rs (deleted), crates/recording/src/lib.rs, crates/recording/src/instant_recording.rs, crates/recording/src/studio_recording.rs, crates/recording/src/capture_pipeline.rs, crates/recording/src/sources/audio_input.rs
Removes old audio_input module and AudioInputFeed API; adds MicrophoneFeed, MicrophoneSamples, MicrophoneFeedLock, error types and actor messages; updates public RecordingBaseInputs to Option<Arc<MicrophoneFeedLock>>; propagates type/signature changes through pipelines, sources, and recording spawn functions.
New CPAL↔FFmpeg bridge
crates/cpal-ffmpeg/Cargo.toml, crates/cpal-ffmpeg/src/lib.rs, crates/media/Cargo.toml
Adds cap-cpal-ffmpeg crate and workspace manifest; introduces DataExt::as_ffmpeg and PlanarData traits to convert CPAL data to FFmpeg audio frames and provide plane access; media crate now depends on this crate.
Recording crate deps and minor edits
crates/recording/Cargo.toml, crates/recording/src/sources/screen_capture/macos.rs
Adds replace_with dependency; removes an unused local binding in macOS screen capture code.
Desktop UI flow tweaks
apps/desktop/src/routes/(window-chrome)/(main).tsx, apps/desktop/src/utils/queries.ts
Initializes mic via commands.setMicInput when provided; camera mutation refactored to expose rawMutate via useMutation wrapped in a Proxy; removes a debug effect and limits audio-level rendering to when a mic is selected.
CLI adjustment for new inputs
apps/cli/src/record.rs
Constructs RecordingBaseInputs using owned mic_feed: None (no borrowed reference).
Direct3D example update
crates/scap-direct3d/examples/cli.rs
Updates Capturer::start usage to new single-closure signature and simplifies example frame handling and settings.
UI typing lint
packages/ui-solid/src/auto-imports.d.ts
Adds a // biome-ignore lint: disable directive; no API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Desktop UI
  participant App as Tauri App
  participant Mic as MicrophoneFeed (actor)
  participant Stream as CPAL Stream Thread
  participant Meter as Audio Meter
  participant Rec as Recording Flow

  UI->>App: setMicInput(label)
  App->>Mic: SetInput { label }
  par build_stream
    Mic-->>Stream: spawn input stream
    Stream-->>Mic: ready / error
    Mic-->>App: reply (Ok / Err)
  end

  Note over App,Meter: subscription
  App-->>Meter: provide flume::Receiver<MicrophoneSamples>
  Stream--)Mic: on_data(MicrophoneSamples)
  Mic--)Meter: broadcast samples

  Note over Rec,Mic: recording lock
  Rec->>Mic: Lock
  Mic-->>Rec: MicrophoneFeedLock (if connected)
  Rec->>Rec: build pipelines with Arc<MicrophoneFeedLock>

  UI->>App: clear mic
  App->>Mic: RemoveInput
  Mic-->>Stream: stop / teardown
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Poem

I twitch my ears at streams that flow,
New actors hop where inputs go.
With flume in paw and bytes in bloom,
I mix the mic, dispel the gloom.
CPAL to FFmpeg—what a leap! 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feed-actors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src-tauri/src/audio_meter.rs (1)

112-145: samples_to_f64: add 24-bit support and properly center unsigned PCM

The current samples_to_f64 match only covers I8/I16/I32/U8/U16/U32/F32/F64 and falls through to _ => todo!(), which will panic on any unhandled format (e.g. I24) and mis-normalizes unsigned PCM by mapping 0..MAX to 0..1 (introducing a DC offset).

Please refactor as follows:

• Add an explicit arm for SampleFormat::I24 before I32 to sign-extend the three bytes into an i32 and divide by 2²³−1.
• Center and normalize all unsigned integer formats:
– U8: let v = u8::from_ne_bytes([data[0]]) as f64; (v - 127.5) / 127.5
– U16: let v = u16::from_ne_bytes([data[0], data[1]]) as f64; (v - 32768.0) / 32767.0
– U32: let v = u32::from_ne_bytes([data[0], data[1], data[2], data[3]]) as f64; (v - 2_147_483_648.0) / 2_147_483_647.0
– (Optionally handle U64 similarly or keep it mapped to 0.0 if your backends never emit it.)
• Replace the wildcard _ => todo!() with a safe fallback (e.g. unreachable!() in release or returning 0.0 + a warning in debug builds) so new or unsupported formats don’t crash.
• Ensure the match remains exhaustive (or add a final arm for SampleFormat::I64/U64 if you choose to keep them).
• (Optional) Add a debug log at startup or in a dev build to print samples.format so you can confirm which formats your device actually produces.

This refactor will prevent runtime panics on 24-bit inputs and eliminate the DC offset in your dBFS calculations.

apps/desktop/src-tauri/src/lib.rs (1)

2219-2234: Avoid awaiting under a write lock in window-destroy path.

Inside WindowEvent::Destroyed, app_state.mic_feed.ask(...).await happens while holding a write lock. Clone the actor, drop the guard, then await; reacquire the guard for subsequent state mutations if needed.

For example:

  • Clone let mic = app_state.mic_feed.clone();
  • Record any booleans you need, then drop(app_state);
  • let _ = mic.ask(microphone::RemoveInput).await;
  • Reacquire let app_state = &mut *state.write().await; to mutate the rest.

If you want, I can provide a concrete diff that reshapes this block accordingly.

🧹 Nitpick comments (26)
crates/recording/Cargo.toml (1)

48-48: Consider whether replace_with is necessary here

It looks like replace_with_or_abort is indeed being used in crates/recording/src/feeds/microphone.rs (around the handle method on line 499), so this dependency isn’t unused—however, it may not be needed if you can achieve the same behavior with the standard library:

  • File and location to review:

    • crates/recording/src/feeds/microphone.rs (use of replace_with_or_abort in async fn handle)
  • Alternative with std:

    • You can swap out the enum state using std::mem::take or std::mem::replace plus manual re-assignment, e.g.:
      let prev = std::mem::take(&mut self.state);
      self.state = match prev {
        State::Locked { inner } => State::Open(OpenState { connecting: None, /* … */ }),
        other => other,
      };
    • This approach avoids an extra crate unless you specifically require panic-safe, abort-on-panic semantics.
  • If you do need the abort-on-panic guarantee that replace_with_or_abort provides, it’s fine to keep the crate. In that case, for consistency across the workspace, consider moving the replace_with = "0.1.8" declaration into the [workspace.dependencies] section of your root Cargo.toml.

apps/desktop/src-tauri/Cargo.toml (1)

104-104: Unify kameo versioning via workspace dependencies

We’ve identified three crates pinning kameo = "0.17.2" explicitly:

  • crates/recording/Cargo.toml (line 46)
  • apps/desktop/src-tauri/Cargo.toml (line 104)
  • crates/media/Cargo.toml (line 42)

To prevent version drift and duplicate feature resolution, consolidate the version in your workspace root and switch each crate to reference it:

• In your root Cargo.toml, add a workspace‐scoped dependency:

[workspace.dependencies]
kameo = "0.17.2"

• Then, in each affected crate’s Cargo.toml, replace the explicit version with a workspace link:

 [dependencies]
- kameo = "0.17.2"
+ kameo.workspace = true

This ensures there’s a single source of truth for the kameo version across all member crates.

crates/cpal-ffmpeg/Cargo.toml (1)

1-12: LGTM: minimal, workspace-aligned crate manifest

The new crate cleanly relies on workspace-managed cpal/ffmpeg and lints. If this crate is internal-only, consider publish = false to avoid accidental publishing. Otherwise, adding license/description/readme is a nice-to-have.

 [package]
 name = "cap-cpal-ffmpeg"
 version = "0.1.0"
 edition = "2024"
+publish = false
+# license = "MIT OR Apache-2.0"
+# description = "Bridge CPAL audio buffers to FFmpeg frames"
+# readme = "README.md"
crates/scap-direct3d/examples/cli.rs (2)

21-24: Confirm intent: border now always shown in example.

Switching is_border_required to Some(true) makes the capture border visible by default. If this was just for local debugging, consider leaving it as None (use library default) or keeping the earlier behavior in the example to avoid surprising users.


27-31: Surface errors from the frame callback or add an error callback equivalent.

The example previously demonstrated error handling via a second callback. With the new one-closure API, any per-frame failure should still be surfaced. Right now, the closure always returns Ok(()), so errors won’t be demonstrated. Consider logging or propagating failures as an example for users.

apps/desktop/src/utils/queries.ts (3)

155-170: Ensure focus action is intentional and non-blocking; consider awaiting or explicitly discarding.

getCurrentWindow().setFocus() is not awaited. If the intent is fire-and-forget, mark it explicitly to avoid unhandled promise noise and make intent clear.

-           getCurrentWindow().setFocus();
+           void getCurrentWindow().setFocus();

163-170: Rollback logic relies on JSON stringification; prefer structural compare.

Using JSON.stringify to compare {ModelID|DeviceID} works today but is brittle to key-order and future extensions. Safer to compare discriminants/fields explicitly.

-           if (JSON.stringify(before) === JSON.stringify(model) || !before) {
+           const same =
+             (!!before && "ModelID" in before && "ModelID" in (model ?? {}) && before.ModelID === (model as any).ModelID) ||
+             (!!before && "DeviceID" in before && "DeviceID" in (model ?? {}) && before.DeviceID === (model as any).DeviceID);
+           if (same || !before) {

176-184: Avoid Proxy to expose rawMutate; return a plain object for stability.

JS Proxy can interfere with type inference, devtools, and reactivity tooling. Prefer returning an object that spreads the mutation and adds rawMutate.

- return new Proxy(
-   setCameraInput as typeof setCameraInput & { rawMutate: typeof rawMutate },
-   {
-     get(target, key) {
-       if (key === "rawMutate") return rawMutate;
-       return Reflect.get(target, key);
-     },
-   },
- );
+ return Object.assign({}, setCameraInput, { rawMutate });
apps/desktop/src/routes/(window-chrome)/(main).tsx (1)

256-258: Guard mic initialization with permission state and handle errors.

We now eagerly set the mic on mount. If OS permission is denied, this may throw in the Tauri command path. Consider checking getPermissions first or catching errors to avoid noisy logs and a bad first-run UX.

- if (rawOptions.micName) commands.setMicInput(rawOptions.micName);
+ if (rawOptions.micName) {
+   commands.setMicInput(rawOptions.micName).catch((e) => {
+     console.warn("Failed to set mic input on mount:", e);
+   });
+ }
crates/cpal-ffmpeg/src/lib.rs (3)

12-14: Validate sample count calculation and frame capacity.

sample_count = bytes.len() / (sample_size * channels) is correct for interleaved buffers, but we should assert the remainder is zero and that the destination frame’s plane/buffer sizes match before copying to avoid potential out-of-bounds on exotic formats.

+        debug_assert_eq!(self.bytes().len() % (sample_size * config.channels as usize), 0, "audio buffer size not aligned to frame");

41-44: Set sample rate before data copy or verify it does not affect buffer allocation.

If the FFmpeg frame’s internal buffer sizing depends on rate (some wrappers derive line sizes from format only, others consult rate), set the rate before copying. If not required, ignore.


30-31: Clippy: unnecessary cast.

sample_size is already usize. Remove the cast.

-                let plane_size = sample_count * sample_size as usize;
+                let plane_size = sample_count * sample_size;
crates/recording/src/feeds/microphone.rs (2)

283-333: Consider moving stream build to a dedicated runtime or threadpool and tighten error signaling.

The raw thread is fine, but:

  • If build_input_stream_raw panics inside CPAL/host callback, BuildStreamCrashed is indistinguishable from early drop. You already map this, but consider more granular logging around the ready channel.
  • When the error callback fires, you actor_ref.kill(). Ensure higher layers expect the actor to die on device errors vs. attempting reconnection.

395-407: Backpressure handling: distinguish Full vs Disconnected and consider dropping or coalescing.

try_send errors other than Disconnected (e.g., Full) are silently ignored, dropping samples without visibility. At minimum, log Full occasionally, or coalesce samples to the latest to avoid unbounded lag.

-            if let Err(TrySendError::Disconnected(_)) = sender.try_send(msg.clone()) {
+            match sender.try_send(msg.clone()) {
+                Ok(_) => {}
+                Err(TrySendError::Full(_)) => {
+                    // Optional: log at trace or count metrics to avoid log spam
+                    trace!("Audio sender {} queue full; dropping frame", i);
+                }
+                Err(TrySendError::Disconnected(_)) => {
+                    warn!("Audio sender {} disconnected, will be removed", i);
+                    to_remove.push(i);
+                }
+            }
apps/desktop/src-tauri/src/recording.rs (2)

355-361: Handle microphone lock edge-cases and avoid indefinite waits.

Good mapping of NoInput → None. Consider:

  • Handling a potential AlreadyLocked (or similar) error by degrading to None rather than failing the start.
  • Adding a short timeout to .ask(...) to avoid hanging if the mic actor stalls.

Example refactor:

-                use kameo::error::SendError;
-                let mic_feed = match state.mic_feed.ask(microphone::Lock).await {
-                    Ok(lock) => Some(Arc::new(lock)),
-                    Err(SendError::HandlerError(microphone::LockFeedError::NoInput)) => None,
-                    Err(e) => return Err(e.to_string()),
-                };
+                use kameo::error::SendError;
+                use tokio::time::{timeout, Duration};
+                let mic_feed = match timeout(Duration::from_secs(2), state.mic_feed.ask(microphone::Lock)).await {
+                    Err(_elapsed) => None, // degrade gracefully on timeout
+                    Ok(Ok(lock)) => Some(Arc::new(lock)),
+                    Ok(Err(SendError::HandlerError(microphone::LockFeedError::NoInput))) => None,
+                    // If applicable in your API, treat AlreadyLocked as "unavailable"
+                    Ok(Err(SendError::HandlerError(microphone::LockFeedError::AlreadyLocked))) => None,
+                    Ok(Err(e)) => return Err(e.to_string()),
+                };

If AlreadyLocked isn’t a thing in your API, keep the first two match arms.


656-657: Don’t block on mic input removal; prefer fire-and-forget or timeout.

On teardown, awaiting RemoveInput can hang shutdown if the actor is slow/stopped. Prefer a non-blocking send or add a timeout and ignore errors.

Example:

-        let _ = app.mic_feed.ask(microphone::RemoveInput).await;
+        use tokio::time::{timeout, Duration};
+        let _ = timeout(Duration::from_secs(1), app.mic_feed.ask(microphone::RemoveInput)).await;

If Kameo supports a one-way send (no reply), use that instead.

crates/recording/src/lib.rs (1)

42-46: Public API change: lifetime removal and Arc-owned mic lock are sound; add Debug and docs.

Moving to owned Option<Arc<MicrophoneFeedLock>> removes borrowing complexity and fits the actor model. To improve debuggability and clarity, derive Debug and document ownership semantics.

-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct RecordingBaseInputs {
     pub capture_target: ScreenCaptureTarget,
     pub capture_system_audio: bool,
     pub mic_feed: Option<Arc<MicrophoneFeedLock>>,
 }

And consider a brief doc comment on mic_feed indicating it can be None when mic is unavailable or not selected.

crates/recording/src/instant_recording.rs (1)

126-132: Prefer trace-level or structured logging for mic info.

Dumping full AudioInfo at debug may be noisy. Consider trace! or structured fields.

-        debug!(
-            "mic audio info: {:#?}",
-            AudioInfo::from_stream_config(mic_feed.config())
-        );
+        trace!(mic = ?AudioInfo::from_stream_config(mic_feed.config()), "mic audio info");
apps/desktop/src-tauri/src/audio_meter.rs (1)

11-11: Nit: naming clarity for amplitude constant.

MAX_AMPLITUDE_F32 actually represents the i16 full-scale magnitude. Consider renaming to MAX_AMPLITUDE_I16 to avoid confusion. Low priority.

Also applies to: 109-110

crates/recording/src/capture_pipeline.rs (1)

127-131: Minor: *audio.audio_info() relies on Copy; alternatively avoid the deref.

If AudioInfo ever stops being Copy, this will break. Optional alternative:

-            let sink = audio_mixer.sink(*audio.audio_info());
+            let sink = audio_mixer.sink(audio.audio_info().clone());

Not required if AudioInfo: Copy is a stable guarantee in your codebase.

Also applies to: 296-301

apps/desktop/src-tauri/src/lib.rs (1)

2016-2037: Keep draining the mic error channel; current task exits after first error.

The spawned task returns after the first recv_async, silently dropping later errors.

Apply this diff:

-        tokio::spawn(async move {
-            let Ok(err) = error_rx.recv_async().await else {
-                return;
-            };
-
-            error!("Mic feed actor error: {err}");
-        });
+        tokio::spawn(async move {
+            while let Ok(err) = error_rx.recv_async().await {
+                error!("Mic feed actor error: {err}");
+            }
+        });
crates/recording/src/studio_recording.rs (2)

744-748: Consider a slightly larger mic channel buffer.

Bound of 8 can drop frames under transient backpressure (disk spikes, scheduler jitter). Studio segments are short-lived but I’d suggest 32–64 for safety, consistent with other audio queues in the codebase.

-        let (tx, rx) = flume::bounded(8);
+        let (tx, rx) = flume::bounded(64);

762-775: Surface encoder errors instead of ignoring them.

mic_encoder.queue_frame(frame.0); return value is ignored; failures will silently discard audio and produce confusing output.

Minimal logging:

-                mic_encoder.queue_frame(frame.0);
+                if let Err(e) = mic_encoder.queue_frame(frame.0) {
+                    tracing::error!("microphone encoder error: {e}");
+                    break;
+                }

Or wire the error to the task result if you want the segment to abort early.

crates/recording/src/sources/audio_input.rs (3)

126-128: Fix log/message text: it’s the microphone feed, not the camera feed.

Apply this diff:

-                            error!("Lost connection with the camera feed");
-                            break Err("Lost connection with the camera feed".to_string());
+                            error!("Lost connection with the microphone feed");
+                            break Err("Lost connection with the microphone feed".to_string());

15-16: Potentially dead code: AudioInputDeviceMap alias is no longer used here.

This type alias (and the Device/SupportedStreamConfig imports) look leftover from the pre-actor design. If it’s not referenced by external modules, consider removing to reduce surface area.


104-117: Ensure correct synchronous actor message API for AddSender

Currently in crates/recording/src/sources/audio_input.rs (lines 113–115), you’re calling:

let _ = self.feed
    .ask(microphone::AddSender(tx))
    .blocking_send();

Per Kameo’s docs, ask(...).blocking_send() will block the current thread until the actor both accepts the message and sends a reply—not just until the mailbox has capacity (docs.page). Since AddSender only registers the sender and doesn’t return any useful data, this extra blocking can stall your audio thread.

Consider using the fire-and-forget API instead:

- let _ = self.feed
-     .ask(microphone::AddSender(tx))
-     .blocking_send();
+ let _ = self.feed
+     .tell(microphone::AddSender(tx))
+     .blocking_send();

This variant blocks only until the message is enqueued (mailbox capacity), avoiding waiting on a reply (docs.page).

• File: crates/recording/src/sources/audio_input.rs
• Lines: 113–115

Also, rather than discarding the SendError, consider logging or handling it to avoid silent failures when adding a sender.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddc965 and dca0b1f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • apps/cli/src/record.rs (1 hunks)
  • apps/desktop/src-tauri/Cargo.toml (1 hunks)
  • apps/desktop/src-tauri/src/audio_meter.rs (3 hunks)
  • apps/desktop/src-tauri/src/lib.rs (8 hunks)
  • apps/desktop/src-tauri/src/recording.rs (3 hunks)
  • apps/desktop/src/routes/(window-chrome)/(main).tsx (2 hunks)
  • apps/desktop/src/utils/queries.ts (2 hunks)
  • crates/cpal-ffmpeg/Cargo.toml (1 hunks)
  • crates/cpal-ffmpeg/src/lib.rs (1 hunks)
  • crates/media/Cargo.toml (1 hunks)
  • crates/recording/Cargo.toml (1 hunks)
  • crates/recording/src/capture_pipeline.rs (6 hunks)
  • crates/recording/src/feeds/audio_input.rs (0 hunks)
  • crates/recording/src/feeds/microphone.rs (1 hunks)
  • crates/recording/src/feeds/mod.rs (1 hunks)
  • crates/recording/src/instant_recording.rs (6 hunks)
  • crates/recording/src/lib.rs (2 hunks)
  • crates/recording/src/sources/audio_input.rs (4 hunks)
  • crates/recording/src/sources/screen_capture/macos.rs (0 hunks)
  • crates/recording/src/studio_recording.rs (9 hunks)
  • crates/scap-direct3d/examples/cli.rs (1 hunks)
  • packages/ui-solid/src/auto-imports.d.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/recording/src/feeds/audio_input.rs
🧰 Additional context used
🧬 Code graph analysis (12)
apps/desktop/src-tauri/src/recording.rs (1)
crates/recording/src/studio_recording.rs (1)
  • new (581-604)
crates/cpal-ffmpeg/src/lib.rs (2)
apps/desktop/src/utils/tauri.ts (1)
  • Audio (359-364)
crates/media-info/src/lib.rs (1)
  • sample_size (92-94)
apps/desktop/src/utils/queries.ts (1)
apps/desktop/src/utils/tauri.ts (3)
  • DeviceOrModelID (516-516)
  • commands (5-305)
  • setCameraInput (9-11)
apps/desktop/src-tauri/src/audio_meter.rs (1)
crates/audio/src/audio_data.rs (1)
  • samples (166-168)
crates/recording/src/feeds/microphone.rs (1)
crates/media-info/src/lib.rs (2)
  • ffmpeg_sample_format_for (256-266)
  • from_stream_config (46-64)
apps/desktop/src/routes/(window-chrome)/(main).tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • commands (5-305)
crates/recording/src/lib.rs (1)
apps/desktop/src/utils/tauri.ts (1)
  • ScreenCaptureTarget (685-688)
crates/recording/src/studio_recording.rs (2)
crates/recording/src/capture_pipeline.rs (1)
  • create_screen_capture (373-394)
crates/recording/src/cursor.rs (1)
  • spawn_cursor_recorder (38-188)
crates/recording/src/instant_recording.rs (3)
crates/media-info/src/lib.rs (1)
  • from_stream_config (46-64)
crates/recording/src/studio_recording.rs (1)
  • new (581-604)
crates/recording/src/capture_pipeline.rs (3)
  • make_instant_mode_pipeline (34-46)
  • make_instant_mode_pipeline (109-224)
  • make_instant_mode_pipeline (273-359)
crates/recording/src/sources/audio_input.rs (1)
crates/media-info/src/lib.rs (1)
  • from_stream_config (46-64)
crates/recording/src/capture_pipeline.rs (1)
crates/recording/src/sources/audio_mixer.rs (1)
  • sink (22-28)
apps/desktop/src-tauri/src/lib.rs (2)
crates/recording/src/studio_recording.rs (1)
  • new (581-604)
apps/desktop/src-tauri/src/audio_meter.rs (2)
  • new (46-52)
  • spawn_event_emitter (18-36)
🪛 GitHub Check: Clippy
crates/cpal-ffmpeg/src/lib.rs

[warning] 30-30: casting to the same type is unnecessary (usize -> usize)
warning: casting to the same type is unnecessary (usize -> usize)
--> crates/cpal-ffmpeg/src/lib.rs:30:49
|
30 | let plane_size = sample_count * sample_size as usize;
| ^^^^^^^^^^^^^^^^^^^^ help: try: sample_size
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast
= note: #[warn(clippy::unnecessary_cast)] on by default

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (20)
packages/ui-solid/src/auto-imports.d.ts (1)

6-6: Ignore generated auto-imports.d.ts via .biomeignore

The path packages/ui-solid/src/auto-imports.d.ts has been added to your .biomeignore, so Biome will now skip linting this generated file. You can safely remove the inline

// biome-ignore lint: disable

directive from the generator output (or update the generator to omit it), since it’s no longer needed.

apps/desktop/src/utils/queries.ts (1)

172-175: Double-check Solid Query API usage: useMutation vs createMutation.

If you upgraded TanStack Solid Query to v5+, useMutation is fine; otherwise, v4 typically uses createMutation. Make sure the package version aligns with this import to avoid runtime/reactivity pitfalls.

Would you like me to scan the repo to confirm the installed version and usages of useMutation vs createMutation?

apps/desktop/src/routes/(window-chrome)/(main).tsx (1)

884-893: LGTM: audio meter only when mic selected.

Showing the level indicator only when a mic is selected reduces confusion and visual noise.

crates/cpal-ffmpeg/src/lib.rs (1)

10-26: I’ve initiated a search for the ffmpeg_sample_format_for function to verify its mapping of CPAL formats to FFmpeg sample layouts. I’ll use the results to confirm whether the review’s concern about a Packed vs. Planar mismatch is valid.

crates/recording/src/feeds/microphone.rs (2)

441-450: Lock handoff is clean; AudioInfo derivation aligns with media-info.

Good use of AudioInfo::from_stream_config(&config) and returning the ActorRef via Deref for ergonomic use, plus Drop-based unlock.


496-509: Unlock retains the attached stream for reuse; good state transition.

The replace_with_or_abort pattern here is clear and avoids intermediate inconsistent states.

crates/recording/src/feeds/mod.rs (1)

2-2: I've requested scripts to verify there are no leftover audio_input files or references specifically within the feeds module. Once we confirm that, we can close out this check.

apps/desktop/src-tauri/src/recording.rs (1)

362-366: LGTM: correct propagation of base inputs to recording actors.

Passing capture_target, capture_system_audio, and the optional mic_feed aligns with the new RecordingBaseInputs.

crates/recording/src/instant_recording.rs (4)

117-119: Signature update aligns with Arc-owned mic feed.

Accepting Option<Arc<MicrophoneFeedLock>> at the pipeline boundary is consistent with the new pipeline builder expectations.


140-144: LGTM: correct propagation of optional mic to pipeline builder.

Passing the Option<Arc<MicrophoneFeedLock>> through matches MakeCapturePipeline::make_instant_mode_pipeline’s signature.


194-199: LGTM: forwarding cloned mic lock to pipeline.

Cloning the Arc before passing into the pipeline is correct; ownership remains shared with the handle.


161-165: No internal usages—confirm external impact and bump version

I searched the repo for both spawn_instant_recording_actor and RecordingBaseInputs and didn’t find any remaining callsites using the old lifetime-parameterized signature. Internally all references appear updated.

However, since dropping the lifetime parameter on spawn_instant_recording_actor and switching to an owned RecordingBaseInputs is an API-breaking change, please:

  • Verify any downstream or external crates consuming this function have been updated accordingly.
  • Bump the crate version (per SemVer, e.g. increment the major version) and note the breaking change in the CHANGELOG.
apps/desktop/src-tauri/src/audio_meter.rs (1)

18-21: API switch to Flume + MicrophoneSamples looks correct.

Receiver type and loop semantics align with the new actor-driven mic feed. No issues spotted here.

crates/recording/src/capture_pipeline.rs (1)

34-47: Migration to Arc<MicrophoneFeedLock> is consistent and sound.

Trait signatures and both macOS/Windows impls correctly adopt Option<Arc<MicrophoneFeedLock>>. This aligns with the new actor lock semantics and avoids lifetime coupling.

Also applies to: 109-121, 273-286

apps/desktop/src-tauri/src/lib.rs (3)

116-118: Good: store actor ref in App and skip serialization.

Using ActorRef<MicrophoneFeed> on state and #[serde(skip)] is the right shape for the new mic architecture.


1111-1112: Device list routing looks good.

Delegating to MicrophoneFeed::list() guarded by the permission check is appropriate.


2185-2186: Good wiring: audio meter now listens to mic samples.

Bounded channel size (8) is reasonable for UI metering.

crates/recording/src/studio_recording.rs (2)

129-135: API change: owned RecordingBaseInputs

Dropping the lifetime parameter simplifies ownership across the actor boundary. Looks good.


158-161: Mic info logging is correct.

Using the lock’s audio_info() for logging avoids rebuilding configs.

crates/recording/src/sources/audio_input.rs (1)

26-41: Switch to Arc<MicrophoneFeedLock> and deriving AudioInfo from stream config looks good.

The start time baseline is computed once and used consistently for timestamping.

Comment on lines +20 to +24
pub struct MicrophoneSamples {
pub data: Vec<u8>,
pub format: SampleFormat,
pub info: InputCallbackInfo,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid per-recipient cloning of audio buffers; switch to Arc<[u8]> or Bytes.

MicrophoneSamples is cloned per sender; cloning a Vec<u8> duplicates the buffer N times. Use a shared backing store to reduce allocations and memory bandwidth.

- pub struct MicrophoneSamples {
-     pub data: Vec<u8>,
+ pub struct MicrophoneSamples {
+     pub data: std::sync::Arc<[u8]>,
      pub format: SampleFormat,
      pub info: InputCallbackInfo,
  }

And in the callback:

- data: data.bytes().to_vec(),
+ data: std::sync::Arc::<[u8]>::from(data.bytes().to_vec().into_boxed_slice()),

Or adopt bytes::Bytes for zero-cost cloning if the ecosystem already uses it.

I can provide a follow-up patch converting the code paths to Arc<[u8]> end-to-end, including consumers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub struct MicrophoneSamples {
pub data: Vec<u8>,
pub format: SampleFormat,
pub info: InputCallbackInfo,
}
// crates/recording/src/feeds/microphone.rs
pub struct MicrophoneSamples {
pub data: std::sync::Arc<[u8]>,
pub format: SampleFormat,
pub info: InputCallbackInfo,
}
🤖 Prompt for AI Agents
In crates/recording/src/feeds/microphone.rs around lines 20 to 24,
MicrophoneSamples currently owns a Vec<u8> which gets cloned per recipient
causing N full buffer copies; change the buffer field to a shared,
cheaply-clonable backing store (e.g. Arc<[u8]> or bytes::Bytes) and update
creation sites to convert the produced Vec into
Arc::from(vec.into_boxed_slice()) or Bytes::from(vec) so clones only clone the
Arc/Bytes handle; update all callsites/traits that expect &Vec<u8> to accept
&[u8] (or Bytes/Arc<[u8]> references), ensure Send+Sync bounds remain satisfied,
and replace any .clone() of the Vec with a cheap clone of the Arc/Bytes so
sending to multiple recipients no longer duplicates the underlying audio buffer.

Comment on lines +119 to +123
c.min_sample_rate().0 <= 48000 && c.max_sample_rate().0 <= 48000
})
.find(|c| ffmpeg_sample_format_for(c.sample_format()).is_some())
})
.and_then(|config| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: device filter incorrectly excludes most devices that support 48 kHz.

c.min_sample_rate().0 <= 48000 && c.max_sample_rate().0 <= 48000 filters out devices whose max > 48k, even if they support 48k. You likely want ranges that include 48k.

-                        .filter(|c| {
-                            c.min_sample_rate().0 <= 48000 && c.max_sample_rate().0 <= 48000
-                        })
+                        .filter(|c| {
+                            let min = c.min_sample_rate().0;
+                            let max = c.max_sample_rate().0;
+                            min <= 48_000 && max >= 48_000
+                        })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c.min_sample_rate().0 <= 48000 && c.max_sample_rate().0 <= 48000
})
.find(|c| ffmpeg_sample_format_for(c.sample_format()).is_some())
})
.and_then(|config| {
.filter(|c| {
let min = c.min_sample_rate().0;
let max = c.max_sample_rate().0;
min <= 48_000 && max >= 48_000
})
.find(|c| ffmpeg_sample_format_for(c.sample_format()).is_some())
🤖 Prompt for AI Agents
In crates/recording/src/feeds/microphone.rs around lines 119 to 123, the device
filter currently uses c.min_sample_rate().0 <= 48000 && c.max_sample_rate().0 <=
48000 which incorrectly excludes devices whose max sample rate is above 48 kHz;
change the condition to check that 48_000 falls within the device's supported
range (i.e., c.min_sample_rate().0 <= 48_000 && c.max_sample_rate().0 >= 48_000)
so devices that support 48 kHz even with a higher max are included.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/desktop/src-tauri/src/lib.rs (2)

220-239: Good: no lock held across await in set_mic_input

Cloning the actor under a read lock, then dropping the guard before awaits, resolves the starvation/deadlock risk noted previously.


2225-2227: Fix: write-lock held across await in window destroy path

app_state (a write guard on Arc<RwLock<App>>) is held while awaiting mic_feed.ask(...). This can block unrelated commands that need the same lock and lead to deadlocks/starvation under load. Clone the actor and compute the condition under the lock, release it, then await and finalize.

-                                    let state = app.state::<Arc<RwLock<App>>>();
-                                    let app_state = &mut *state.write().await;
-
-                                    if !app_state.is_recording_active_or_pending() {
-                                        let _ =
-                                            app_state.mic_feed.ask(microphone::RemoveInput).await;
-                                        app_state.camera_feed.take();
-
-                                        if let Some(camera) = CapWindowId::Camera.get(&app) {
-                                            let _ = camera.close();
-                                        }
-                                    }
+                                    let state = app.state::<Arc<RwLock<App>>>();
+                                    // Extract needed data while holding the lock, then drop it before awaiting.
+                                    let (should_clear, mic) = {
+                                        let mut app_state = state.write().await;
+                                        let should_clear = !app_state.is_recording_active_or_pending();
+                                        let mic = app_state.mic_feed.clone();
+                                        if should_clear {
+                                            app_state.camera_feed.take();
+                                        }
+                                        (should_clear, mic)
+                                    };
+
+                                    if should_clear {
+                                        let _ = mic.ask(microphone::RemoveInput).await;
+                                        if let Some(camera) = CapWindowId::Camera.get(&app) {
+                                            let _ = camera.close();
+                                        }
+                                    }
🧹 Nitpick comments (3)
apps/desktop/src-tauri/src/lib.rs (3)

116-118: Mark mic_feed as non-IPC type explicitly

#[serde(skip)] prevents serialization, but to avoid Specta attempting to derive a TS type for ActorRef<...>, also mark it with #[specta(skip)] for clarity and to future-proof changes.

     #[serde(skip)]
+    #[specta(skip)]
     mic_feed: ActorRef<feeds::microphone::MicrophoneFeed>,

1104-1112: Return a deterministic, sorted device list

For stable UI and testability, sort device names before returning. If MicrophoneFeed::list() isn’t ordered, consumers will otherwise see non-deterministic ordering.

-    Ok(MicrophoneFeed::list().keys().cloned().collect())
+    let mut devices: Vec<_> = MicrophoneFeed::list().keys().cloned().collect();
+    devices.sort_unstable();
+    Ok(devices)

2016-2037: Harden mic actor wiring: drain error channel and log AddSender failures

  • The error handler only logs the first error and then exits; drain the channel in a loop so subsequent errors aren’t lost.
  • Don’t ignore a failed AddSender—log it so issues in the audio meter pipeline are observable.
-    let (mic_samples_tx, mic_samples_rx) = flume::bounded(8);
+    let (mic_samples_tx, mic_samples_rx) = flume::bounded(8);

     let mic_feed = {
         let (error_tx, error_rx) = flume::bounded(1);

-        let mic_feed = MicrophoneFeed::spawn(MicrophoneFeed::new(error_tx));
+        let mic_feed = MicrophoneFeed::spawn(MicrophoneFeed::new(error_tx));

-        // TODO: make this part of a global actor one day
-        tokio::spawn(async move {
-            let Ok(err) = error_rx.recv_async().await else {
-                return;
-            };
-
-            error!("Mic feed actor error: {err}");
-        });
+        // TODO: make this part of a global actor one day
+        tokio::spawn(async move {
+            while let Ok(err) = error_rx.recv_async().await {
+                error!("Mic feed actor error: {err}");
+            }
+        });

-        let _ = mic_feed
-            .ask(feeds::microphone::AddSender(mic_samples_tx))
-            .await;
+        if let Err(e) = mic_feed
+            .ask(feeds::microphone::AddSender(mic_samples_tx))
+            .await
+        {
+            error!("Failed to add mic_samples sender to mic feed: {e}");
+        }

         mic_feed
     };

-    audio_meter::spawn_event_emitter(app.clone(), mic_samples_rx);
+    audio_meter::spawn_event_emitter(app.clone(), mic_samples_rx);

Also applies to: 2185-2186

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dca0b1f and 058f7d8.

📒 Files selected for processing (1)
  • apps/desktop/src-tauri/src/lib.rs (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
apps/desktop/src-tauri/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Use tauri_specta on the Rust side for IPC (typed commands/events) and emit events via generated types

Files:

  • apps/desktop/src-tauri/src/lib.rs
🧬 Code graph analysis (1)
apps/desktop/src-tauri/src/lib.rs (3)
apps/desktop/src-tauri/src/audio_meter.rs (2)
  • new (46-52)
  • spawn_event_emitter (18-36)
crates/recording/src/studio_recording.rs (1)
  • new (581-604)
crates/editor/src/editor_instance.rs (1)
  • new (34-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
apps/desktop/src-tauri/src/lib.rs (1)

30-39: Actor-based microphone integration imports look correct

  • Bringing kameo::Actor into scope enables method-style MicrophoneFeed::spawn(...); ActorRef is used as expected.
  • IPC remains typed via tauri_specta macros and collected commands/events, aligning with the repo guideline for apps/desktop/src-tauri/**/*.rs.

Also applies to: 42-48, 53-56, 61-72, 81-85, 87-88

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants