diff --git a/electron/windows.ts b/electron/windows.ts index dcd9f92bd..026f8a72a 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -62,6 +62,10 @@ export function createHudOverlayWindow(): BrowserWindow { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } + // Use "floating" level so macOS system dialogs (e.g. screen recording + // permission prompt) are not hidden behind the HUD overlay. + win.setAlwaysOnTop(true, "floating"); + win.webContents.on("did-finish-load", () => { win?.webContents.send("main-process-message", new Date().toLocaleString()); }); @@ -167,6 +171,9 @@ export function createSourceSelectorWindow(): BrowserWindow { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } + // Use "floating" level so macOS system dialogs appear above this window. + win.setAlwaysOnTop(true, "floating"); + if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL + "?windowType=source-selector"); } else { diff --git a/package-lock.json b/package-lock.json index ba40beb4d..f83d45eee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@sapphi-red/web-noise-suppressor": "^0.3.5", "@types/gif.js": "^0.2.5", "@uiw/color-convert": "^2.9.2", "@uiw/react-color-block": "^2.9.2", @@ -4459,6 +4460,12 @@ "win32" ] }, + "node_modules/@sapphi-red/web-noise-suppressor": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@sapphi-red/web-noise-suppressor/-/web-noise-suppressor-0.3.5.tgz", + "integrity": "sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA==", + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -15699,9 +15706,9 @@ } }, "node_modules/vite-plugin-electron": { - "version": "0.28.8", - "resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.28.8.tgz", - "integrity": "sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg==", + "version": "0.28.6", + "resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.28.6.tgz", + "integrity": "sha512-DANntooA/XcUQuaOG7tQ0nnWh8iP5yKur2e9GDafjslOPAVZehRyrbi2UEI6rlIhN6hHwcqAjY+/Zz8+thAL5g==", "dev": true, "license": "MIT", "peerDependencies": { @@ -15714,9 +15721,9 @@ } }, "node_modules/vite-plugin-electron-renderer": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", - "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.5.tgz", + "integrity": "sha512-EQ7ORuPp8vFPCqfuGnVo7d36fXS0IFH4/RUlKb1drseix3TQEPcgwEuFADdXBxRgqMp70njz/1m0kdf5lEsm8w==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index d41fd4005..d0bb0c931 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,15 @@ }, "scripts": { "dev": "vite", - "build": "tsc && vite build && electron-builder", + "build": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder", "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", "i18n:check": "node scripts/i18n-check.mjs", "preview": "vite preview", - "build:mac": "tsc && vite build && electron-builder --mac", - "build:win": "tsc && vite build && electron-builder --win", - "build:linux": "tsc && vite build && electron-builder --linux AppImage deb", + "build:mac": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder --mac", + "build:win": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder --win", + "build:linux": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder --linux AppImage deb", "test": "vitest --run", "test:watch": "vitest", "build-vite": "tsc && vite build", @@ -46,6 +46,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@sapphi-red/web-noise-suppressor": "^0.3.5", "@types/gif.js": "^0.2.5", "@uiw/color-convert": "^2.9.2", "@uiw/react-color-block": "^2.9.2", diff --git a/scripts/fix-electron-esm.mjs b/scripts/fix-electron-esm.mjs new file mode 100644 index 000000000..a5e2f8ba7 --- /dev/null +++ b/scripts/fix-electron-esm.mjs @@ -0,0 +1,24 @@ +#!/usr/bin/env node +/** + * Post-build fix: Places a { "type": "commonjs" } package.json inside + * dist-electron/ so Node.js treats .js files there as CJS — even though + * the root package.json has "type": "module". + * + * vite-plugin-electron/simple already emits CJS output for the main process + * when it detects "type": "module", but the file extension stays .js and + * Node resolves the nearest package.json to decide the module type. + * This script bridges that gap. + */ +import fs from "node:fs"; +import path from "node:path"; + +const distElectron = path.resolve("dist-electron"); +const target = path.join(distElectron, "package.json"); + +if (!fs.existsSync(distElectron)) { + console.error("[fix-electron-esm] dist-electron/ not found after vite build."); + process.exit(1); +} + +fs.writeFileSync(target, JSON.stringify({ type: "commonjs" }, null, 2) + "\n"); +console.log("[fix-electron-esm] Wrote dist-electron/package.json (type: commonjs)"); diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 4fb419364..5648de906 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -232,6 +232,12 @@ interface SettingsPanelProps { webcamSizePreset?: WebcamSizePreset; onWebcamSizePresetChange?: (size: WebcamSizePreset) => void; onWebcamSizePresetCommit?: () => void; + noiseReductionEnabled?: boolean; + onNoiseReductionEnabledChange?: (enabled: boolean) => void; + noiseReductionLevel?: import("@/lib/audioEnhancement").NoiseReductionLevel; + onNoiseReductionLevelChange?: ( + level: import("@/lib/audioEnhancement").NoiseReductionLevel, + ) => void; } export default SettingsPanel; @@ -324,6 +330,10 @@ export function SettingsPanel({ webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET, onWebcamSizePresetChange, onWebcamSizePresetCommit, + noiseReductionEnabled = false, + onNoiseReductionEnabledChange, + noiseReductionLevel = "moderate", + onNoiseReductionLevelChange, }: SettingsPanelProps) { const t = useScopedT("settings"); const [wallpaperPaths, setWallpaperPaths] = useState([]); @@ -1065,6 +1075,65 @@ export function SettingsPanel({ + + +
+ + + + + + + + + {t("audio.title")} +
+
+ +
+ + +
+ {noiseReductionEnabled && ( +
+ +
+ {(["light", "moderate", "aggressive"] as const).map((lvl) => ( + + ))} +
+

{t("audio.nrDescription")}

+
+ )} +
+
+ (null); const [isFullscreen, setIsFullscreen] = useState(false); + const [noiseReductionEnabled, setNoiseReductionEnabled] = useState(false); + const [noiseReductionLevel, setNoiseReductionLevel] = + useState("moderate"); const playerContainerRef = useRef(null); const videoPlaybackRef = useRef(null); @@ -1857,6 +1860,8 @@ export default function VideoEditor() { onBlurDataChange={handleBlurDataPreviewChange} onBlurDataCommit={commitState} cursorTelemetry={cursorTelemetry} + noiseReductionEnabled={noiseReductionEnabled} + noiseReductionLevel={noiseReductionLevel} /> @@ -2051,6 +2056,10 @@ export default function VideoEditor() { onZoomDurationChange={(zoomIn, zoomOut) => selectedZoomId && handleZoomDurationChange(selectedZoomId, zoomIn, zoomOut) } + noiseReductionEnabled={noiseReductionEnabled} + onNoiseReductionEnabledChange={setNoiseReductionEnabled} + noiseReductionLevel={noiseReductionLevel} + onNoiseReductionLevelChange={setNoiseReductionLevel} /> diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index b798641e4..9428f4e41 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -18,6 +18,7 @@ import { useRef, useState, } from "react"; +import { useAudioEnhancement } from "@/hooks/useAudioEnhancement"; import { getAssetPath } from "@/lib/assetPath"; import { getWebcamLayoutCssBoxShadow, @@ -110,6 +111,8 @@ interface VideoPlaybackProps { onBlurDataChange?: (id: string, blurData: BlurData) => void; onBlurDataCommit?: () => void; cursorTelemetry?: import("./types").CursorTelemetryPoint[]; + noiseReductionEnabled?: boolean; + noiseReductionLevel?: import("@/lib/audioEnhancement").NoiseReductionLevel; } export interface VideoPlaybackRef { @@ -168,10 +171,13 @@ const VideoPlayback = forwardRef( onBlurDataChange, onBlurDataCommit, cursorTelemetry = [], + noiseReductionEnabled = false, + noiseReductionLevel = "moderate", }, ref, ) => { const videoRef = useRef(null); + const [videoElement, setVideoElement] = useState(null); const webcamVideoRef = useRef(null); const containerRef = useRef(null); const appRef = useRef(null); @@ -179,6 +185,9 @@ const VideoPlayback = forwardRef( const videoContainerRef = useRef(null); const cameraContainerRef = useRef(null); const timeUpdateAnimationRef = useRef(null); + // Post-processing noise reduction on playback audio + useAudioEnhancement(videoElement, noiseReductionEnabled, noiseReductionLevel); + const [pixiReady, setPixiReady] = useState(false); const [videoReady, setVideoReady] = useState(false); const [overlaySize, setOverlaySize] = useState({ width: 800, height: 600 }); @@ -1083,6 +1092,7 @@ const VideoPlayback = forwardRef( const handleLoadedMetadata = (e: React.SyntheticEvent) => { const video = e.currentTarget; + setVideoElement(video); onDurationChange(video.duration); video.currentTime = 0; video.pause(); diff --git a/src/hooks/useAudioEnhancement.ts b/src/hooks/useAudioEnhancement.ts new file mode 100644 index 000000000..04421712b --- /dev/null +++ b/src/hooks/useAudioEnhancement.ts @@ -0,0 +1,147 @@ +import { useCallback, useEffect, useRef } from "react"; +import { + type AudioEnhancementNodes, + createAudioEnhancementChain, + type NoiseReductionLevel, +} from "@/lib/audioEnhancement"; + +export type { NoiseReductionLevel } from "@/lib/audioEnhancement"; + +/** + * Hook that applies real-time audio enhancement (noise reduction) to a + *