Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,7 @@ interface Window {
success: boolean;
microphoneEnabled: boolean;
microphoneDeviceId?: string;
noiseSuppressionMode?: string;
systemAudioEnabled: boolean;
}>;
getRecordingAudioLabConfig: () => Promise<{
Expand All @@ -889,6 +890,7 @@ interface Window {
setRecordingPreferences: (prefs: {
microphoneEnabled?: boolean;
microphoneDeviceId?: string;
noiseSuppressionMode?: string;
systemAudioEnabled?: boolean;
}) => Promise<{ success: boolean; error?: string }>;
/** Countdown timer before recording */
Expand Down
21 changes: 21 additions & 0 deletions electron/ipc/register/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const BROWSER_MICROPHONE_PROFILES = new Set([
"raw",
]);

/**
* Reads the browser microphone processing profile requested through the lab env flag.
*/
function getBrowserMicrophoneProfileFromEnv() {
const requested = process.env[BROWSER_MICROPHONE_PROFILE_ENV]?.trim() || null;
const normalized = requested?.toLowerCase() ?? DEFAULT_BROWSER_MICROPHONE_PROFILE;
Expand All @@ -42,6 +45,9 @@ function getBrowserMicrophoneProfileFromEnv() {
};
}

/**
* Loads persisted app settings, falling back to an empty store when the file is absent or invalid.
*/
function readAppSettingsStore(): Record<string, unknown> {
try {
const content = readFileSync(APP_SETTINGS_FILE, "utf-8");
Expand All @@ -56,14 +62,23 @@ function readAppSettingsStore(): Record<string, unknown> {
}
}

/**
* Persists the complete app settings store to disk.
*/
function writeAppSettingsStore(store: Record<string, unknown>) {
writeFileSync(APP_SETTINGS_FILE, JSON.stringify(store, null, 2), "utf-8");
}

/**
* Checks for an explicitly persisted app setting without treating falsy values as missing.
*/
function hasAppSetting(store: Record<string, unknown>, key: string): boolean {
return Reflect.getOwnPropertyDescriptor(store, key) !== undefined;
}

/**
* Registers settings, preferences, shortcuts, countdown, and update IPC handlers.
*/
export function registerSettingsHandlers() {
ipcMain.handle("app:getVersion", () => {
return app.getVersion();
Expand Down Expand Up @@ -153,13 +168,18 @@ export function registerSettingsHandlers() {
typeof parsed.microphoneDeviceId === "string"
? parsed.microphoneDeviceId
: undefined,
noiseSuppressionMode:
typeof parsed.noiseSuppressionMode === "string"
? parsed.noiseSuppressionMode
: "rnnoise",
systemAudioEnabled: parsed.systemAudioEnabled === true,
};
} catch {
return {
success: true,
microphoneEnabled: false,
microphoneDeviceId: undefined,
noiseSuppressionMode: "rnnoise",
systemAudioEnabled: false,
};
}
Expand All @@ -176,6 +196,7 @@ export function registerSettingsHandlers() {
prefs: {
microphoneEnabled?: boolean;
microphoneDeviceId?: string;
noiseSuppressionMode?: string;
systemAudioEnabled?: boolean;
},
) => {
Expand Down
Binary file modified electron/native/bin/darwin-arm64/recordly-native-cursor-monitor
Binary file not shown.
Binary file not shown.
Binary file modified electron/native/bin/darwin-arm64/recordly-system-cursors
Binary file not shown.
Binary file modified electron/native/bin/darwin-arm64/recordly-window-list
Binary file not shown.
Binary file modified electron/native/bin/darwin-x64/recordly-native-cursor-monitor
Binary file not shown.
Binary file modified electron/native/bin/darwin-x64/recordly-screencapturekit-helper
Binary file not shown.
Binary file modified electron/native/bin/darwin-x64/recordly-system-cursors
Binary file not shown.
Binary file modified electron/native/bin/darwin-x64/recordly-window-list
Binary file not shown.
1 change: 1 addition & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
setRecordingPreferences: (prefs: {
microphoneEnabled?: boolean;
microphoneDeviceId?: string;
noiseSuppressionMode?: string;
systemAudioEnabled?: boolean;
}) => ipcRenderer.invoke("set-recording-preferences", prefs),
getCountdownDelay: () => ipcRenderer.invoke("get-countdown-delay"),
Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"@sapphi-red/speex-preprocess-wasm": "^0.4.0",
"@shiguredo/rnnoise-wasm": "^2025.1.5",
"capturekit": "^1.0.13",
"electron-updater": "^6.8.3",
"ffmpeg-static": "^5.3.0",
Expand Down
10 changes: 10 additions & 0 deletions src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import { MarqueeText } from "./SourceSelector";

const SHOW_DEV_UPDATE_PREVIEW = import.meta.env.DEV;

/**
* Mounts the launch HUD with shared popover coordination for recording controls.
*/
export function LaunchWindow() {
return (
<LaunchPopoverCoordinatorProvider>
Expand All @@ -52,6 +55,9 @@ export function LaunchWindow() {
);
}

/**
* Renders the interactive launch HUD content once popover coordination is available.
*/
function LaunchWindowContent() {
const t = useScopedT("launch");
const { openId, requestClose, requestOpen } = useLaunchPopoverCoordinator();
Expand All @@ -69,6 +75,8 @@ function LaunchWindowContent() {
setMicrophoneEnabled,
microphoneDeviceId,
setMicrophoneDeviceId,
noiseSuppressionMode,
setNoiseSuppressionMode,
systemAudioEnabled,
setSystemAudioEnabled,
webcamEnabled,
Expand Down Expand Up @@ -262,6 +270,8 @@ function LaunchWindowContent() {
devices={devices}
microphoneDeviceId={microphoneDeviceId}
selectedDeviceId={selectedDeviceId}
noiseSuppressionMode={noiseSuppressionMode}
onSelectNoiseSuppressionMode={setNoiseSuppressionMode}
onSelectDevice={(deviceId) => {
setMicrophoneEnabled(true);
setSelectedDeviceId(deviceId);
Expand Down
95 changes: 87 additions & 8 deletions src/components/launch/popovers/MicPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { MicrophoneSlashIcon, SpeakerHighIcon, SpeakerXIcon } from "@phosphor-icons/react";
import {
CheckCircleIcon,
MicrophoneIcon,
MicrophoneSlashIcon,
SpeakerHighIcon,
SpeakerXIcon,
} from "@phosphor-icons/react";
import type { ReactElement } from "react";
import { useScopedT } from "@/contexts/I18nContext";
import { DropdownItem, HudPopover, MicDeviceRow } from "./PopoverScaffold";
import type { NoiseSuppressionMode } from "@/lib/audio/noiseSuppression";
import styles from "../LaunchWindow.module.css";
import { useLaunchPopoverCoordinator } from "./LaunchPopoverCoordinator";
import type { DeviceOption } from "./launchPopoverTypes";
import type { ReactElement } from "react";
import styles from "../LaunchWindow.module.css";
import { DropdownItem, HudPopover, MicDeviceRow } from "./PopoverScaffold";

const POPOVER_ID = "mic";

/**
* Renders microphone, system audio, and noise suppression controls for the recording HUD.
*/
export function MicPopover({
trigger,
disabled,
Expand All @@ -19,6 +29,8 @@ export function MicPopover({
microphoneDeviceId,
selectedDeviceId,
onSelectDevice,
noiseSuppressionMode,
onSelectNoiseSuppressionMode,
}: {
trigger: ReactElement;
disabled?: boolean;
Expand All @@ -30,6 +42,8 @@ export function MicPopover({
microphoneDeviceId?: string;
selectedDeviceId?: string;
onSelectDevice: (deviceId: string) => void;
noiseSuppressionMode: NoiseSuppressionMode;
onSelectNoiseSuppressionMode: (mode: NoiseSuppressionMode) => void;
}) {
const t = useScopedT("launch");
const { isOpen, requestOpen, requestClose } = useLaunchPopoverCoordinator();
Expand All @@ -53,7 +67,9 @@ export function MicPopover({
>
<div className={styles.ddLabel}>{t("recording.microphone")}</div>
<DropdownItem
icon={systemAudioEnabled ? <SpeakerHighIcon size={16} /> : <SpeakerXIcon size={16} />}
icon={
systemAudioEnabled ? <SpeakerHighIcon size={16} /> : <SpeakerXIcon size={16} />
}
selected={systemAudioEnabled}
onClick={onToggleSystemAudio}
>
Expand All @@ -73,22 +89,85 @@ export function MicPopover({
</DropdownItem>
)}
{!microphoneEnabled && (
<div className="px-3 py-2 text-xs text-[var(--launch-text-muted)]">{t("recording.selectMicToEnable")}</div>
<div className="px-3 py-2 text-xs text-[var(--launch-text-muted)]">
{t("recording.selectMicToEnable")}
</div>
)}
{devices.map((device) => (
<MicDeviceRow
key={device.deviceId}
device={device}
selected={
microphoneEnabled &&
(microphoneDeviceId === device.deviceId || selectedDeviceId === device.deviceId)
(microphoneDeviceId === device.deviceId ||
selectedDeviceId === device.deviceId)
}
onSelect={() => onSelectDevice(device.deviceId)}
/>
))}
{devices.length === 0 && (
<div className="text-center text-xs text-[var(--launch-text-muted)] py-4">{t("recording.noMicrophonesFound")}</div>
<div className="text-center text-xs text-[var(--launch-text-muted)] py-4">
{t("recording.noMicrophonesFound")}
</div>
)}
<div className={styles.ddLabel}>{t("recording.noiseSuppression")}</div>
<NoiseSuppressionOption
mode="rnnoise"
selectedMode={noiseSuppressionMode}
label={t("recording.noiseSuppressionRnnoise")}
description={t("recording.noiseSuppressionRnnoiseDescription")}
onSelect={onSelectNoiseSuppressionMode}
/>
<NoiseSuppressionOption
mode="speex"
selectedMode={noiseSuppressionMode}
label={t("recording.noiseSuppressionSpeex")}
description={t("recording.noiseSuppressionSpeexDescription")}
onSelect={onSelectNoiseSuppressionMode}
/>
<NoiseSuppressionOption
mode="disabled"
selectedMode={noiseSuppressionMode}
label={t("recording.noiseSuppressionDisabled")}
description={t("recording.noiseSuppressionDisabledDescription")}
onSelect={onSelectNoiseSuppressionMode}
/>
</HudPopover>
);
}

/**
* Renders one selectable noise suppression mode in the microphone popover.
*/
function NoiseSuppressionOption({
mode,
selectedMode,
label,
description,
onSelect,
}: {
mode: NoiseSuppressionMode;
selectedMode: NoiseSuppressionMode;
label: string;
description: string;
onSelect: (mode: NoiseSuppressionMode) => void;
}) {
const selected = mode === selectedMode;
return (
<button
type="button"
className={`${styles.ddItem} ${selected ? styles.ddItemSelected : ""} items-start`}
onClick={() => onSelect(mode)}
>
<span className="shrink-0 mt-0.5">
{selected ? <CheckCircleIcon size={16} /> : <MicrophoneIcon size={16} />}
</span>
<span className="min-w-0 flex-1 text-left">
<span className="block truncate">{label}</span>
<span className="block whitespace-normal text-[11px] leading-4 text-[var(--launch-text-muted)]">
{description}
</span>
</span>
</button>
);
}
Loading