diff --git a/.github/workflows/sync-tests.yml b/.github/workflows/sync-tests.yml
index b6f6731801d..6367e4c7b13 100644
--- a/.github/workflows/sync-tests.yml
+++ b/.github/workflows/sync-tests.yml
@@ -26,6 +26,7 @@ on:
- "crates/editor/**"
- "crates/export/**"
- "crates/audio/**"
+ - "crates/rnnoise/**"
- "crates/media-info/**"
- "crates/project/**"
- "scripts/recording-reliability.py"
@@ -87,6 +88,9 @@ jobs:
shell: pwsh
run: Add-Content -Path $env:GITHUB_PATH -Value "${{ github.workspace }}\\target\\ffmpeg\\bin"
+ - name: RNNoise lifecycle regression tests
+ run: cargo test --locked -p cap-rnnoise --lib
+
- name: Recording metadata and harness regression tests
shell: bash
run: |
diff --git a/Cargo.lock b/Cargo.lock
index 280a91700af..a7f205fd772 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -387,12 +387,6 @@ version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
-[[package]]
-name = "anymap3"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9"
-
[[package]]
name = "apple-native-keyring-store"
version = "1.0.1"
@@ -445,12 +439,6 @@ dependencies = [
"syn 2.0.106",
]
-[[package]]
-name = "array-init"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
-
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -1281,10 +1269,10 @@ name = "cap-audio"
version = "0.1.0"
dependencies = [
"cap-enc-ffmpeg",
+ "cap-rnnoise",
"cidre",
"cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)",
"ffmpeg-next",
- "nnnoiseless",
"serde",
"serde_json",
"tempfile",
@@ -1360,6 +1348,7 @@ dependencies = [
"libloading 0.9.0",
"ndarray 0.16.1",
"ort",
+ "tokio",
"tracing",
"wgpu",
]
@@ -1999,6 +1988,13 @@ dependencies = [
"workspace-hack",
]
+[[package]]
+name = "cap-rnnoise"
+version = "0.1.0"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "cap-test"
version = "0.1.0"
@@ -3446,19 +3442,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
-[[package]]
-name = "easyfft"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "767e39eef2ad8a3b6f1d733be3ec70364d21d437d06d4f18ea76ce08df20b75f"
-dependencies = [
- "array-init",
- "generic_singleton",
- "num-complex",
- "realfft",
- "rustfft",
-]
-
[[package]]
name = "either"
version = "1.15.0"
@@ -4165,16 +4148,6 @@ dependencies = [
"version_check",
]
-[[package]]
-name = "generic_singleton"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab6e923c8e978e57cf63e2e200ca967d1d20f0ea2662b28f6d4e11c44aa6ab16"
-dependencies = [
- "anymap3",
- "parking_lot",
-]
-
[[package]]
name = "gethostname"
version = "1.0.2"
@@ -6415,16 +6388,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "nnnoiseless"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "805d5964d1e7a0006a7fdced7dae75084d66d18b35f1dfe81bd76929b1f8da0c"
-dependencies = [
- "easyfft",
- "once_cell",
-]
-
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -6519,7 +6482,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
- "serde",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 91e45dbbdc9..0aa206efbb2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -103,6 +103,12 @@ debug = 1
[profile.dev.package."*"]
debug = 0
+[profile.dev.package.cap-camera-effects]
+opt-level = 2
+
+[profile.dev.package.cap-audio]
+opt-level = 2
+
# Optimize for smaller binary size
[profile.release]
panic = "unwind"
diff --git a/apps/cli/src/selftest/playback.rs b/apps/cli/src/selftest/playback.rs
index 6612739e7a7..57e997d9471 100644
--- a/apps/cli/src/selftest/playback.rs
+++ b/apps/cli/src/selftest/playback.rs
@@ -903,6 +903,8 @@ mod fixture {
timescale: 1.0,
name: None,
speed_audio_mode: None,
+ hide_cursor: None,
+ volume: None,
}],
transitions: Vec::new(),
zoom_segments: Vec::new(),
diff --git a/apps/desktop-gpui/Cargo.lock b/apps/desktop-gpui/Cargo.lock
index 2a9f3e5aa36..cf9255478dc 100644
--- a/apps/desktop-gpui/Cargo.lock
+++ b/apps/desktop-gpui/Cargo.lock
@@ -453,12 +453,6 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
-[[package]]
-name = "anymap3"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9"
-
[[package]]
name = "ar_archive_writer"
version = "0.5.3"
@@ -485,12 +479,6 @@ dependencies = [
"syn 2.0.119",
]
-[[package]]
-name = "array-init"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
-
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -1409,10 +1397,10 @@ name = "cap-audio"
version = "0.1.0"
dependencies = [
"cap-enc-ffmpeg",
+ "cap-rnnoise",
"cidre",
"cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)",
"ffmpeg-next",
- "nnnoiseless",
"serde",
"serde_json",
"tokio",
@@ -1631,12 +1619,14 @@ dependencies = [
"ffmpeg-next",
"flume 0.11.1",
"futures",
+ "image 0.25.10",
"lru",
"ringbuf",
"sentry",
"serde",
"serde_json",
"specta",
+ "tempfile",
"tokio",
"tokio-util",
"tracing",
@@ -1962,6 +1952,13 @@ dependencies = [
"workspace-hack",
]
+[[package]]
+name = "cap-rnnoise"
+version = "0.1.0"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "cap-timestamp"
version = "0.1.0"
@@ -3422,19 +3419,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
-[[package]]
-name = "easyfft"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "767e39eef2ad8a3b6f1d733be3ec70364d21d437d06d4f18ea76ce08df20b75f"
-dependencies = [
- "array-init",
- "generic_singleton",
- "num-complex",
- "realfft",
- "rustfft",
-]
-
[[package]]
name = "either"
version = "1.17.0"
@@ -4151,16 +4135,6 @@ dependencies = [
"version_check",
]
-[[package]]
-name = "generic_singleton"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab6e923c8e978e57cf63e2e200ca967d1d20f0ea2662b28f6d4e11c44aa6ab16"
-dependencies = [
- "anymap3",
- "parking_lot",
-]
-
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -5909,6 +5883,17 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+[[package]]
+name = "lewton"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
+dependencies = [
+ "byteorder",
+ "ogg",
+ "tinyvec",
+]
+
[[package]]
name = "libappindicator"
version = "0.9.0"
@@ -6717,16 +6702,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "nnnoiseless"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "805d5964d1e7a0006a7fdced7dae75084d66d18b35f1dfe81bd76929b1f8da0c"
-dependencies = [
- "easyfft",
- "once_cell",
-]
-
[[package]]
name = "no_std_io2"
version = "0.9.4"
@@ -6848,7 +6823,6 @@ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"bytemuck",
"num-traits",
- "serde",
]
[[package]]
@@ -7427,6 +7401,15 @@ dependencies = [
"cc",
]
+[[package]]
+name = "ogg"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
+dependencies = [
+ "byteorder",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -9117,6 +9100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb"
dependencies = [
"cpal 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lewton",
"symphonia",
"thiserror 1.0.69",
]
diff --git a/apps/desktop-gpui/Cargo.toml b/apps/desktop-gpui/Cargo.toml
index b2411ea5a9f..c90bcce4a19 100644
--- a/apps/desktop-gpui/Cargo.toml
+++ b/apps/desktop-gpui/Cargo.toml
@@ -68,7 +68,7 @@ cap-project = { path = "../../crates/project" }
# a `RenderImage` (`src/editor_sidebar/cursor.rs`).
cap-cursor-info = { path = "../../crates/cursor-info" }
resvg = "0.45"
-rodio = { version = "0.19.0", default-features = false, features = ["mp3"] }
+rodio = { version = "0.19.0", default-features = false, features = ["mp3", "vorbis"] }
# The editor stack. `cap-rendering` is not a new subtree -- cap-recording
# already depends on it, so wgpu 25 and its vendored wgpu-hal have been built
# by this workspace since the recording unit; naming it here only makes the
diff --git a/apps/desktop-gpui/assets/icons/arrow-left-to-line.svg b/apps/desktop-gpui/assets/icons/arrow-left-to-line.svg
new file mode 100644
index 00000000000..acacb0348d7
--- /dev/null
+++ b/apps/desktop-gpui/assets/icons/arrow-left-to-line.svg
@@ -0,0 +1 @@
+
diff --git a/apps/desktop-gpui/assets/icons/arrow-right-to-line.svg b/apps/desktop-gpui/assets/icons/arrow-right-to-line.svg
new file mode 100644
index 00000000000..5f334654ece
--- /dev/null
+++ b/apps/desktop-gpui/assets/icons/arrow-right-to-line.svg
@@ -0,0 +1 @@
+
diff --git a/apps/desktop-gpui/assets/icons/diamond.svg b/apps/desktop-gpui/assets/icons/diamond.svg
deleted file mode 100644
index e5ac92404a8..00000000000
--- a/apps/desktop-gpui/assets/icons/diamond.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/apps/desktop-gpui/assets/icons/gem.svg b/apps/desktop-gpui/assets/icons/gem.svg
new file mode 100644
index 00000000000..a69fafa6503
--- /dev/null
+++ b/apps/desktop-gpui/assets/icons/gem.svg
@@ -0,0 +1 @@
+
diff --git a/apps/desktop-gpui/assets/icons/italic.svg b/apps/desktop-gpui/assets/icons/italic.svg
deleted file mode 100644
index 21b1ab754c1..00000000000
--- a/apps/desktop-gpui/assets/icons/italic.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/desktop-gpui/assets/icons/mouse-pointer-ban.svg b/apps/desktop-gpui/assets/icons/mouse-pointer-ban.svg
new file mode 100644
index 00000000000..d3e6a7126e1
--- /dev/null
+++ b/apps/desktop-gpui/assets/icons/mouse-pointer-ban.svg
@@ -0,0 +1 @@
+
diff --git a/apps/desktop-gpui/dev.sh b/apps/desktop-gpui/dev.sh
index cd92c3edd60..49aa48d645e 100755
--- a/apps/desktop-gpui/dev.sh
+++ b/apps/desktop-gpui/dev.sh
@@ -10,7 +10,7 @@ STATE_FILE="$PWD/target/dev-restore.json"
BIN="$PWD/target/debug/cap-gpui"
BUILD=cargo
-WATCH_PATHS=(src assets Cargo.toml)
+WATCH_PATHS=(src assets Cargo.toml ../desktop/src-tauri/sounds)
[ -d resources ] && WATCH_PATHS+=(resources)
for crate in camera scap-targets recording timestamp utils project rendering editor export; do
[ -d "../../crates/$crate/src" ] && WATCH_PATHS+=("../../crates/$crate/src")
diff --git a/apps/desktop-gpui/src/app_sounds.rs b/apps/desktop-gpui/src/app_sounds.rs
new file mode 100644
index 00000000000..1604ab84354
--- /dev/null
+++ b/apps/desktop-gpui/src/app_sounds.rs
@@ -0,0 +1,98 @@
+use std::io::Cursor;
+
+use anyhow::Context as _;
+use rodio::{Decoder, OutputStream, Sink};
+
+#[path = "../../desktop/src-tauri/src/recording_start_sound.rs"]
+mod recording_start_sound;
+
+pub use recording_start_sound::should_play_countdown_sound;
+
+pub async fn play_recording_start_sound() {
+ recording_start_sound::play(AppSound::StartRecording.bytes()).await;
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum AppSound {
+ StartRecording,
+ StopRecording,
+ Notification,
+}
+
+impl AppSound {
+ fn bytes(self) -> &'static [u8] {
+ match self {
+ Self::StartRecording => {
+ include_bytes!("../../desktop/src-tauri/sounds/start-recording.ogg")
+ }
+ Self::StopRecording => {
+ include_bytes!("../../desktop/src-tauri/sounds/stop-recording.ogg")
+ }
+ Self::Notification => include_bytes!("../../desktop/src-tauri/sounds/action.ogg"),
+ }
+ }
+
+ pub fn play(self) {
+ if let Err(error) = std::thread::Builder::new()
+ .name("app-sound".into())
+ .spawn(move || {
+ if let Err(error) = self.play_to_end() {
+ tracing::warn!(?self, %error, "App sound unavailable");
+ }
+ })
+ {
+ tracing::warn!(%error, "Could not start app sound playback");
+ }
+ }
+
+ fn play_to_end(self) -> anyhow::Result<()> {
+ let (_stream, handle) = OutputStream::try_default().context("opening audio output")?;
+ let source = Decoder::new(Cursor::new(self.bytes())).context("decoding app sound")?;
+ let sink = Sink::try_new(&handle).context("creating app sound sink")?;
+ sink.append(source);
+ sink.sleep_until_end();
+ tracing::debug!(?self, "App sound played");
+ Ok(())
+ }
+}
+
+pub fn play_notification() {
+ if crate::store::notification_sounds_enabled() {
+ AppSound::Notification.play();
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use rodio::Source as _;
+
+ const SOUNDS: [AppSound; 3] = [
+ AppSound::StartRecording,
+ AppSound::StopRecording,
+ AppSound::Notification,
+ ];
+
+ #[test]
+ fn tauri_sound_assets_decode_to_audible_pcm() {
+ for sound in SOUNDS {
+ let source = Decoder::new(Cursor::new(sound.bytes())).unwrap();
+ let channels = source.channels();
+ let rate = source.sample_rate();
+ assert!(channels > 0);
+ assert!(rate > 0);
+ let samples: Vec<_> = source.collect();
+ assert!(samples.iter().any(|sample| *sample != 0), "{sound:?}");
+ let duration = samples.len() as f64 / f64::from(channels) / f64::from(rate);
+ assert!((0.05..10.0).contains(&duration), "{sound:?}: {duration}");
+ }
+ }
+
+ #[test]
+ #[ignore = "plays the recording and notification cues through the default audio output"]
+ fn native_app_sound_playback() {
+ for sound in SOUNDS {
+ sound.play_to_end().unwrap();
+ }
+ }
+}
diff --git a/apps/desktop-gpui/src/app_windows.rs b/apps/desktop-gpui/src/app_windows.rs
index d7ddafcc65a..a65d1b11c25 100644
--- a/apps/desktop-gpui/src/app_windows.rs
+++ b/apps/desktop-gpui/src/app_windows.rs
@@ -2971,7 +2971,7 @@ pub(crate) fn refresh_linux_instant_camera(
effects: LinuxCameraProcessing {
mirrored: snapshot.state.mirrored,
blur: match snapshot.state.background_blur {
- BlurMode::Off => LinuxCameraBlur::Off,
+ BlurMode::Off | BlurMode::Remove => LinuxCameraBlur::Off,
BlurMode::Light => LinuxCameraBlur::Light,
BlurMode::Heavy => LinuxCameraBlur::Heavy,
},
@@ -4052,6 +4052,16 @@ fn camera_frame(cx: &mut App) -> Option {
.filter(|camera| camera.snapshot.bounds.width > 0. && camera.snapshot.bounds.height > 0.)
}
+pub(crate) fn studio_camera_snapshot(
+ target: &ScreenCaptureTarget,
+ cx: &mut App,
+) -> Option {
+ let handle = cx.global::().camera?;
+ handle
+ .update(cx, |view, window, _| view.studio_snapshot(window, target))
+ .ok()
+}
+
fn visible_camera_frame(cx: &mut App) -> Option {
if camera_preview_is_inline(cx) {
return None;
@@ -5234,6 +5244,23 @@ fn load_editor_project_candidate(
}
async fn drive_auto_export(handle: WindowHandle, cx: &mut gpui::AsyncApp) {
+ if let Ok(page) = std::env::var("CAP_GPUI_AUTO_EXPORT_PAGE") {
+ cx.background_executor()
+ .timer(std::time::Duration::from_millis(300))
+ .await;
+ let _ = handle.update(cx, |view, window, cx| {
+ view.open_export(window, cx);
+ if let Some(export) = view.export.as_mut() {
+ let flags: Vec<&str> = page.split(',').collect();
+ export.advanced_open = flags.contains(&"advanced");
+ if flags.contains(&"link") {
+ export.destination = crate::editor_export::ExportDestination::Link;
+ }
+ }
+ cx.notify();
+ });
+ return;
+ }
let Some(path) = std::env::var_os("CAP_GPUI_AUTO_EXPORT").map(PathBuf::from) else {
return;
};
@@ -5354,8 +5381,9 @@ fn load_editor_waveforms(
}
/// `CAP_GPUI_AUTO_SIDEBAR=[:]` selects a config-sidebar tab and
-/// optionally scrolls its body, and `CAP_GPUI_AUTO_SELECT=