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=:[,]` -/// selects timeline segments so their panel opens. +/// optionally scrolls its body, `CAP_GPUI_AUTO_CAMERA3D=add[: