Skip to content

Commit f64d8f1

Browse files
authored
Merge pull request #485 from OpenWhispr/core-audio-tap
feat(audio): add native macOS system audio tap
2 parents 0892c71 + 217c7e2 commit f64d8f1

22 files changed

Lines changed: 1306 additions & 574 deletions

electron-builder.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"extraResources": [
8787
".env",
8888
"src/assets/**/*",
89+
"resources/bin/macos-audio-tap",
8990
"resources/bin/macos-globe-listener",
9091
"resources/bin/macos-fast-paste",
9192
"resources/bin/macos-mic-listener",

main.js

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,6 @@ if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland")
8989
app.commandLine.appendSwitch("enable-features", "UseOzonePlatform,WaylandWindowDecorations");
9090
}
9191

92-
// Enable system audio loopback capture on macOS via CoreAudio Taps (Electron 39+).
93-
// Without these flags, getDisplayMedia({ audio: "loopback" }) returns a live audio track
94-
// that produces only silence (all-zero samples).
95-
if (process.platform === "darwin") {
96-
app.commandLine.appendSwitch(
97-
"enable-features",
98-
"MacLoopbackAudioForScreenShare,MacSckSystemAudioLoopbackOverride,MacCatapSystemAudioLoopbackCapture"
99-
);
100-
}
101-
10292
// Set desktop filename so Wayland compositors can match windows to the .desktop entry.
10393
// This allows XDG portals (e.g. PipeWire) to persist permissions across sessions.
10494
if (process.platform === "linux") {
@@ -197,6 +187,7 @@ const WhisperCudaManager = require("./src/helpers/whisperCudaManager");
197187
const GoogleCalendarManager = require("./src/helpers/googleCalendarManager");
198188
const MeetingProcessDetector = require("./src/helpers/meetingProcessDetector");
199189
const AudioActivityDetector = require("./src/helpers/audioActivityDetector");
190+
const AudioTapManager = require("./src/helpers/audioTapManager");
200191
const MeetingDetectionEngine = require("./src/helpers/meetingDetectionEngine");
201192
const { i18nMain, changeLanguage } = require("./src/helpers/i18nMain");
202193
const { ensureYdotool } = require("./src/helpers/ensureYdotool");
@@ -218,6 +209,7 @@ let textEditMonitor = null;
218209
let whisperCudaManager = null;
219210
let googleCalendarManager = null;
220211
let meetingDetectionEngine = null;
212+
let audioTapManager = null;
221213
let ipcHandlers = null;
222214
let globeKeyAlertShown = false;
223215
let authBridgeServer = null;
@@ -294,6 +286,7 @@ function initializeCoreManagers() {
294286
updateManager.setWindowManager(windowManager);
295287
windowsKeyManager = new WindowsKeyManager();
296288
textEditMonitor = new TextEditMonitor();
289+
audioTapManager = new AudioTapManager();
297290
windowManager.textEditMonitor = textEditMonitor;
298291

299292
// IPC handlers must be registered before window content loads
@@ -310,6 +303,7 @@ function initializeCoreManagers() {
310303
whisperCudaManager,
311304
googleCalendarManager,
312305
meetingDetectionEngine,
306+
audioTapManager,
313307
getTrayManager: () => trayManager,
314308
});
315309
}
@@ -534,28 +528,6 @@ async function startApp() {
534528
}
535529
);
536530

537-
// Handle getDisplayMedia() calls from the renderer — auto-select the first
538-
// screen source with loopback audio so no system picker dialog is shown.
539-
const { desktopCapturer } = require("electron");
540-
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
541-
try {
542-
const sources = await desktopCapturer.getSources({ types: ["screen"] });
543-
debugLogger.debug("Display media sources", {
544-
count: sources.length,
545-
names: sources.map((s) => s.name),
546-
});
547-
if (!sources.length) {
548-
debugLogger.error("No screen sources available — System Audio permission may be denied");
549-
callback({});
550-
return;
551-
}
552-
callback({ video: sources[0], audio: "loopback" });
553-
} catch (err) {
554-
debugLogger.error("Display media handler error", { error: err.message });
555-
callback({});
556-
}
557-
});
558-
559531
windowManager.setActivationModeCache(environmentManager.getActivationMode());
560532
windowManager.setFloatingIconAutoHide(environmentManager.getFloatingIconAutoHide());
561533
windowManager.setPanelStartPosition(environmentManager.getPanelStartPosition());
@@ -1150,6 +1122,9 @@ if (gotSingleInstanceLock) {
11501122
if (googleCalendarManager) {
11511123
googleCalendarManager.stop();
11521124
}
1125+
if (audioTapManager) {
1126+
audioTapManager.stop().catch(() => {});
1127+
}
11531128
if (ipcHandlers) {
11541129
ipcHandlers._cleanupTextEditMonitor();
11551130
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"compile:linux-paste": "node scripts/build-linux-fast-paste.js",
1717
"compile:media-remote": "node scripts/build-media-remote.js",
1818
"compile:mic-listener": "node scripts/build-macos-mic-listener.js",
19-
"compile:native": "npm run compile:globe && npm run compile:fast-paste && npm run compile:winkeys && npm run compile:winpaste && npm run compile:linux-paste && npm run compile:text-monitor && npm run compile:media-remote && npm run compile:mic-listener",
19+
"compile:audio-tap": "node scripts/build-macos-audio-tap.js",
20+
"compile:native": "npm run compile:globe && npm run compile:fast-paste && npm run compile:winkeys && npm run compile:winpaste && npm run compile:linux-paste && npm run compile:text-monitor && npm run compile:media-remote && npm run compile:mic-listener && npm run compile:audio-tap",
2021
"prestart": "npm run compile:native",
2122
"start": "electron .",
2223
"predev": "npm run compile:native",

preload.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
170170
// Clipboard functions
171171
checkAccessibilityPermission: (silent) =>
172172
ipcRenderer.invoke("check-accessibility-permission", silent),
173-
promptAccessibilityPermission: () =>
174-
ipcRenderer.invoke("prompt-accessibility-permission"),
173+
promptAccessibilityPermission: () => ipcRenderer.invoke("prompt-accessibility-permission"),
175174
readClipboard: () => ipcRenderer.invoke("read-clipboard"),
176175
writeClipboard: (text) => ipcRenderer.invoke("write-clipboard", text),
177176
checkPasteTools: () => ipcRenderer.invoke("check-paste-tools"),
@@ -362,7 +361,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
362361

363362
// System settings helpers for microphone/audio permissions
364363
requestMicrophoneAccess: () => ipcRenderer.invoke("request-microphone-access"),
364+
checkMicrophoneAccess: () => ipcRenderer.invoke("check-microphone-access"),
365365
checkSystemAudioAccess: () => ipcRenderer.invoke("check-system-audio-access"),
366+
requestSystemAudioAccess: () => ipcRenderer.invoke("request-system-audio-access"),
366367
openMicrophoneSettings: () => ipcRenderer.invoke("open-microphone-settings"),
367368
openSoundInputSettings: () => ipcRenderer.invoke("open-sound-input-settings"),
368369
openAccessibilitySettings: () => ipcRenderer.invoke("open-accessibility-settings"),
@@ -607,9 +608,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
607608
gcalGetUpcomingEvents: (windowMinutes) =>
608609
ipcRenderer.invoke("gcal-get-upcoming-events", windowMinutes),
609610

610-
// Desktop audio capture
611-
getDesktopSources: (types) => ipcRenderer.invoke("get-desktop-sources", types),
612-
613611
// Google Calendar event listeners
614612
onGcalMeetingStarting: registerListener(
615613
"gcal-meeting-starting",

0 commit comments

Comments
 (0)