Native Node.js addon for capturing per-application audio on macOS using ScreenCaptureKit framework. Real-time audio streaming with event-based API.
npm install screencapturekit-audio-capture> Native Node.js addon for capturing per-application audio on macOS using the ScreenCaptureKit framework



Capture real-time audio from any macOS application with a simple, event-driven API. Built with N-API for Node.js compatibility and ScreenCaptureKit for system-level audio access.
---
- Features
- Requirements
- Installation
- Project Structure
- Quick Start
- Quick Integration Guide
- Module Exports
- Testing
- Stream-Based API
- API Reference
- Multi-Process Capture Service
- Events Reference
- TypeScript
- Working with Audio Data
- Resource Lifecycle
- Common Issues
- Examples
- Platform Support
- Performance
- Contributing
- License
---
- šµ Per-App Audio Capture - Isolate audio from specific applications, windows, or displays
- š Multi-Source Capture - Capture from multiple apps simultaneously with mixed output
- ā” Real-Time Streaming - Low-latency callbacks with Event or Stream-based APIs
- š Multi-Process Service - Server/client architecture for sharing audio across processes
- š Audio Utilities - Built-in RMS/peak/dB analysis and WAV file export
- š TypeScript-First - Full type definitions with memory-safe resource cleanup
- macOS 13.0 (Ventura) or later
- Node.js 14.0.0 or later (Node.js 18+ recommended for running the automated test suite)
- Screen Recording permission (granted in System Preferences)
``bash`
npm install screencapturekit-audio-capture
Prebuilt binaries are included ā no compilation or Xcode required for Apple Silicon M series (ARM64) machines.
If no prebuild is available for your architecture, the addon will compile from source automatically. This requires:
- Xcode Command Line Tools (minimum version 14.0)
`bash`
xcode-select --install
- macOS SDK 13.0 or later
The build process links these macOS frameworks:
- ScreenCaptureKit - Per-application audio capture
- AVFoundation - Audio processing
- CoreMedia - Media sample handling
- CoreVideo - Video frame handling
- Foundation - Core Objective-C runtime
All frameworks are part of the macOS system and require no additional installation.
When installed from npm, the package includes:
- src/ - TypeScript SDK source code and native C++/Objective-C++ codedist/
- - Compiled JavaScript and TypeScript declarationsbinding.gyp
- - Native build configurationREADME.md
- , LICENSE, CHANGELOG.md
Note: Example files are available in the GitHub repository but are not included in the npm package to reduce installation size.
See npm ls screencapturekit-audio-capture for installation location.
``
screencapturekit-audio-capture/
āāā src/ # Source code
ā āāā capture/ # Core audio capture functionality
ā ā āāā index.ts # Barrel exports
ā ā āāā audio-capture.ts # Main AudioCapture class
ā ā āāā audio-stream.ts # Readable stream wrapper
ā ā
ā āāā native/ # Native C++/Objective-C++ code
ā ā āāā addon.mm # Node.js N-API bindings
ā ā āāā wrapper.h # C++ header
ā ā āāā wrapper.mm # ScreenCaptureKit implementation
ā ā
ā āāā service/ # Multi-process capture service
ā ā āāā index.ts # Barrel exports
ā ā āāā server.ts # WebSocket server for shared capture
ā ā āāā client.ts # WebSocket client
ā ā
ā āāā utils/ # Utility modules
ā ā āāā index.ts # Barrel exports
ā ā āāā stt-converter.ts # Speech-to-text transform stream
ā ā āāā native-loader.ts # Native addon loader
ā ā
ā āāā core/ # Shared types, errors, and lifecycle
ā ā āāā index.ts # Barrel exports
ā ā āāā types.ts # TypeScript type definitions
ā ā āāā errors.ts # Error classes and codes
ā ā āāā cleanup.ts # Resource cleanup utilities
ā ā
ā āāā index.ts # Main package exports
ā
āāā dist/ # Compiled JavaScript output
āāā tests/ # Test suites (unit, integration, edge-cases)
āāā readme_examples/ # Runnable example scripts
āāā prebuilds/ # Prebuilt native binaries
āāā build/ # Native compilation output
ā
āāā package.json # Package manifest
āāā tsconfig.json # TypeScript configuration
āāā binding.gyp # Native addon build configuration
āāā CHANGELOG.md # Version history
āāā LICENSE # MIT License
āāā README.md # This file
> š See readme_examples/basics/01-quick-start.ts for runnable code
`typescript
import { AudioCapture } from 'screencapturekit-audio-capture';
const capture = new AudioCapture();
const app = capture.selectApp(['Spotify', 'Music', 'Safari'], { fallbackToFirst: true });
capture.on('audio', (sample) => {
console.log(Volume: ${AudioCapture.rmsToDb(sample.rms).toFixed(1)} dB);
});
capture.startCapture(app.processId);
setTimeout(() => capture.stopCapture(), 10000);
`
> š All integration patterns below have runnable examples in readme_examples/
Common patterns for integrating audio capture into your application:
| Pattern | Example File | Description |
|---------|--------------|-------------|
| STT Integration | voice/02-stt-integration.ts | Stream + event-based approaches for speech-to-text |
| Voice Agent | voice/03-voice-agent.ts | Real-time processing with low-latency config |
| Recording | voice/04-audio-recording.ts | Capture to WAV file with efficient settings |
| Robust Capture | basics/05-robust-capture.ts | Production error handling with fallbacks |
| Multi-App | capture-targets/13-multi-app-capture.ts | Capture game + Discord, Zoom + Music, etc. |
| Multi-Process | advanced/20-capture-service.ts | Share audio across multiple processes |
| Graceful Cleanup | advanced/21-graceful-cleanup.ts | Resource lifecycle and cleanup utilities |
For STT engines:
`typescript`
{ format: 'int16', channels: 1, minVolume: 0.01 } // Int16 mono, silence filtered
For low-latency voice processing:
`typescript`
{ format: 'int16', channels: 1, bufferSize: 1024, minVolume: 0.005 }
For recording:
`typescript`
{ format: 'int16', channels: 2, bufferSize: 4096 } // Stereo, larger buffer for stability
| Property | Type | Description |
|----------|------|-------------|
| data | Buffer | Audio data (Float32 or Int16) |sampleRate
| | number | Sample rate in Hz (e.g., 48000) |channels
| | number | 1 = mono, 2 = stereo |format
| | 'float32' \| 'int16' | Audio format |rms
| | number | RMS volume (0.0-1.0) |peak
| | number | Peak volume (0.0-1.0) |timestamp
| | number | Timestamp in seconds |durationMs
| | number | Duration in milliseconds |sampleCount
| | number | Total samples across all channels |framesCount
| | number | Frames per channel |
`typescript
import { AudioCapture, AudioCaptureError, ErrorCode } from 'screencapturekit-audio-capture';
import type { AudioSample, ApplicationInfo } from 'screencapturekit-audio-capture';
// Multi-process capture service (for sharing audio across processes)
import { AudioCaptureServer, AudioCaptureClient } from 'screencapturekit-audio-capture';
// Resource cleanup utilities
import { cleanupAll, getActiveInstanceCount, installGracefulShutdown } from 'screencapturekit-audio-capture';
`
| Export | Description |
|--------|-------------|
| AudioCapture | High-level event-based API (recommended) |AudioStream
| | Readable stream (via createAudioStream()) |STTConverter
| | Transform stream for STT (via createSTTStream()) |AudioCaptureServer
| | WebSocket server for shared capture (multi-process) |AudioCaptureClient
| | WebSocket client to receive shared audio |AudioCaptureError
| | Error class with codes and details |ErrorCode
| | Error code enum for type-safe handling |cleanupAll
| | Dispose all AudioCapture and AudioCaptureServer instances |getActiveInstanceCount
| | Get total active instance count |installGracefulShutdown
| | Install process exit handlers for cleanup |ScreenCaptureKit
| | Low-level native binding (advanced) |
Types: AudioSample, ApplicationInfo, WindowInfo, DisplayInfo, CaptureOptions, PermissionStatus, ActivityInfo, ServerOptions, ClientOptions, and more.
Note: Test files are available in the GitHub repository but are not included in the npm package.
Tests are written in TypeScript and live under tests/. They use Node's built-in test runner with tsx (Node 18+).
Test Commands:
- npm test ā Runs every suite in tests/*/.test.ts (unit, integration, edge-cases) against the mocked ScreenCaptureKit layer; works cross-platform.npm run test:unit
- ā Fast coverage for utilities, audio metrics, selection, and capture control.npm run test:integration
- ā Multi-component flows (window/display capture, activity tracking, capability guards) using the shared mock.npm run test:edge-cases
- ā Boundary/error handling coverage.
Type Checking:
- npm run typecheck ā Type-check the SDK source code.npm run typecheck:tests
- ā Type-check the test files.
For true hardware validation, run the example scripts on macOS with Screen Recording permission enabled.
> š See readme_examples/streams/06-stream-basics.ts and readme_examples/streams/07-stream-processing.ts for runnable examples
Use Node.js Readable streams for composable audio processing:
`typescript
const audioStream = capture.createAudioStream('Spotify', { minVolume: 0.01 });
audioStream.pipe(yourWritableStream);
// Object mode for metadata access
const metaStream = capture.createAudioStream('Spotify', { objectMode: true });
metaStream.on('data', (sample) => console.log(RMS: ${sample.rms}));`
| Use Case | Recommended API |
|----------|----------------|
| Piping through transforms | Stream |
| Backpressure handling | Stream |
| Multiple listeners | Event |
| Maximum simplicity | Event |
Both APIs use the same underlying capture mechanism and have identical performance.
1. Always handle errors - Attach an error handler to prevent crashespipeline()
2. Use - Better error handling than chaining .pipe()stream.stop()
3. Clean up resources - Call when donedata
4. Choose the right mode - Normal mode for raw data, object mode for metadata
5. Stream must flow - Attach a listener to start capture
`typescript
import { pipeline } from 'stream';
// Recommended pattern
pipeline(audioStream, transform, writable, (err) => {
if (err) console.error('Pipeline failed:', err);
});
// Always handle SIGINT
process.on('SIGINT', () => audioStream.stop());
`
| Issue | Cause | Solution |
|-------|-------|----------|
| "Application not found" | App not running | Use selectApp() with fallbacks |minVolume
| No data events | App not playing audio / too high | Verify app is playing; lower or remove threshold |pipeline()
| "stream.push() after EOF" | Stopping abruptly | Use for proper cleanup |AudioCapture
| "Already capturing" | Multiple streams from one instance | Create separate instances |data
| Memory growing | Not consuming data | Attach listener; use circular buffer |
- Normal mode is faster than object mode (no metadata calculation)
- Batch processing is more efficient than per-sample processing
- Default highWaterMark is suitable for most cases
> š See readme_examples/streams/07-stream-processing.ts for a complete production-ready stream example
High-level event-based API (recommended).
#### Methods Overview
| # | Category | Method | Description |
|---|----------|--------|-------------|
| | Discovery | | |
| 1 | | getApplications(opts?) | List all capturable apps |getAudioApps(opts?)
| 2 | | | List apps likely to produce audio |findApplication(id)
| 3 | | | Find app by name or bundle ID |findByName(name)
| 4 | | | Alias for findApplication() |getApplicationByPid(pid)
| 5 | | | Find app by process ID |getWindows(opts?)
| 6 | | | List all capturable windows |getDisplays()
| 7 | | | List all displays |selectApp(ids?, opts?)
| | Selection | | |
| 8 | | | Smart app selection with fallbacks |startCapture(app, opts?)
| | Capture | | |
| 9 | | | Start capturing from an app |captureWindow(id, opts?)
| 10 | | | Capture from a specific window |captureDisplay(id, opts?)
| 11 | | | Capture from a display |captureMultipleApps(ids, opts?)
| 12 | | | Capture multiple apps (mixed) |captureMultipleWindows(ids, opts?)
| 13 | | | Capture multiple windows (mixed) |captureMultipleDisplays(ids, opts?)
| 14 | | | Capture multiple displays (mixed) |stopCapture()
| 15 | | | Stop current capture |isCapturing()
| 16 | | | Check if currently capturing |getStatus()
| 17 | | | Get detailed capture status |getCurrentCapture()
| 18 | | | Get current capture target info |createAudioStream(app, opts?)
| | Streams | | |
| 19 | | | Create Node.js Readable stream |createSTTStream(app?, opts?)
| 20 | | | Stream pre-configured for STT |enableActivityTracking(opts?)
| | Activity | | |
| 21 | | | Track which apps produce audio |disableActivityTracking()
| 22 | | | Stop tracking and clear cache |getActivityInfo()
| 23 | | | Get tracking stats |dispose()
| | Lifecycle | | |
| 24 | | | Release resources and stop capture |isDisposed()
| 25 | | | Check if instance is disposed |
#### Static Methods
| # | Method | Description |
|---|--------|-------------|
| S1 | AudioCapture.verifyPermissions() | Check screen recording permission |AudioCapture.bufferToFloat32Array(buf)
| S2 | | Convert Buffer to Float32Array |AudioCapture.rmsToDb(rms)
| S3 | | Convert RMS (0-1) to decibels |AudioCapture.peakToDb(peak)
| S4 | | Convert peak (0-1) to decibels |AudioCapture.calculateDb(buf, method?)
| S5 | | Calculate dB from audio buffer |AudioCapture.writeWav(buf, opts)
| S6 | | Create WAV file from PCM data |AudioCapture.cleanupAll()
| S7 | | Dispose all active instances |AudioCapture.getActiveInstanceCount()
| S8 | | Get number of active instances |
#### Events
| Event | Payload | Description |
|-------|---------|-------------|
| 'start' | CaptureInfo | Capture started |'audio'
| | AudioSample | Audio data received |'stop'
| | CaptureInfo | Capture stopped |'error'
| | AudioCaptureError | Error occurred |
---
#### Discovery Methods
##### [1] getApplications(options?): ApplicationInfo[]
List all capturable applications.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| includeEmpty | boolean | false | Include apps with empty names (helpers, background services) |
`typescript`
const apps = capture.getApplications();
const allApps = capture.getApplications({ includeEmpty: true });
---
##### [2] getAudioApps(options?): ApplicationInfo[]
List apps likely to produce audio. Filters system apps, utilities, and background processes.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| includeSystemApps | boolean | false | Include system apps (Finder, etc.) |includeEmpty
| | boolean | false | Include apps with empty names |sortByActivity
| | boolean | false | Sort by recent audio activity (requires [21]) |appList
| | Array | null | Reuse prefetched app list |
`typescript
const audioApps = capture.getAudioApps();
// Returns: ['Spotify', 'Safari', 'Music', 'Zoom']
// Excludes: Finder, Terminal, System Preferences, etc.
// Sort by activity (most active first)
capture.enableActivityTracking();
const sorted = capture.getAudioApps({ sortByActivity: true });
`
---
##### [3] findApplication(identifier): ApplicationInfo | null
Find app by name or bundle ID (case-insensitive, partial match).
| Parameter | Type | Description |
|-----------|------|-------------|
| identifier | string | App name or bundle ID |
`typescript`
const spotify = capture.findApplication('Spotify');
const safari = capture.findApplication('com.apple.Safari');
const partial = capture.findApplication('spot'); // Matches "Spotify"
---
##### [4] findByName(name): ApplicationInfo | null
Alias for findApplication(). Provided for semantic clarity.
---
##### [5] getApplicationByPid(processId): ApplicationInfo | null
Find app by process ID.
| Parameter | Type | Description |
|-----------|------|-------------|
| processId | number | Process ID |
`typescript`
const app = capture.getApplicationByPid(12345);
---
##### [6] getWindows(options?): WindowInfo[]
List all capturable windows.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| onScreenOnly | boolean | false | Only include visible windows |requireTitle
| | boolean | false | Only include windows with titles |processId
| | number | - | Filter by owning process ID |
Returns WindowInfo:
- windowId: Unique window identifiertitle
- : Window titleowningProcessId
- : PID of owning appowningApplicationName
- : App nameowningBundleIdentifier
- : Bundle IDframe
- : { x, y, width, height }layer
- : Window layer levelonScreen
- : Whether visibleactive
- : Whether active
`typescript${w.windowId}: ${w.title} (${w.owningApplicationName})
const windows = capture.getWindows({ onScreenOnly: true, requireTitle: true });
windows.forEach(w => console.log());`
---
##### [7] getDisplays(): DisplayInfo[]
List all displays.
Returns DisplayInfo:
- displayId: Unique display identifierwidth
- : Display width in pixelsheight
- : Display height in pixelsframe
- : { x, y, width, height }isMainDisplay
- : Whether this is the primary display
`typescript`
const displays = capture.getDisplays();
const main = displays.find(d => d.isMainDisplay);
---
#### Selection Method
##### [8] selectApp(identifiers?, options?): ApplicationInfo | null
Smart app selection with multiple fallback strategies.
| Parameter | Type | Description |
|-----------|------|-------------|
| identifiers | string \| number \| Array \| null | App name, PID, bundle ID, or array to try in order |
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| audioOnly | boolean | true | Only search audio apps |fallbackToFirst
| | boolean | false | Return first app if no match |throwOnNotFound
| | boolean | false | Throw error instead of returning null |sortByActivity
| | boolean | false | Sort by recent activity (requires [21]) |appList
| | Array | null | Reuse prefetched app list |
`typescript
// Try multiple apps in order
const app = capture.selectApp(['Spotify', 'Music', 'Safari']);
// Get first audio app
const first = capture.selectApp();
// Fallback to first if none match
const fallback = capture.selectApp(['Spotify'], { fallbackToFirst: true });
// Throw on failure
try {
const app = capture.selectApp(['Spotify'], { throwOnNotFound: true });
} catch (err) {
console.log('Not found:', err.details.availableApps);
}
`
---
#### Capture Methods
All capture methods accept CaptureOptions:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| format | 'float32' \| 'int16' | 'float32' | Audio format |channels
| | 1 \| 2 | 2 | Mono or stereo |sampleRate
| | number | 48000 | Requested sample rate (system-dependent) |bufferSize
| | number | system | Buffer size in frames (affects latency) |minVolume
| | number | 0 | Min RMS threshold (0-1), filters silence |excludeCursor
| | boolean | true | Reserved for future video features |
Buffer Size Guidelines:
- 1024: ~21ms latency, higher CPU2048
- : ~43ms latency, balanced (recommended)4096
- : ~85ms latency, lower CPU
---
##### [9] startCapture(appIdentifier, options?): boolean
Start capturing from an application.
| Parameter | Type | Description |
|-----------|------|-------------|
| appIdentifier | string \| number \| ApplicationInfo | App name, bundle ID, PID, or app object |
`typescript
capture.startCapture('Spotify'); // By name
capture.startCapture('com.spotify.client'); // By bundle ID
capture.startCapture(12345); // By PID
capture.startCapture(app); // By object
// With options
capture.startCapture('Spotify', {
format: 'int16',
channels: 1,
minVolume: 0.01
});
`
---
##### [10] captureWindow(windowId, options?): boolean
Capture audio from a specific window.
| Parameter | Type | Description |
|-----------|------|-------------|
| windowId | number | Window ID from getWindows() |
`typescript`
const windows = capture.getWindows({ requireTitle: true });
const target = windows.find(w => w.title.includes('Safari'));
capture.captureWindow(target.windowId, { format: 'int16' });
---
##### [11] captureDisplay(displayId, options?): boolean
Capture audio from a display.
| Parameter | Type | Description |
|-----------|------|-------------|
| displayId | number | Display ID from getDisplays() |
`typescript`
const displays = capture.getDisplays();
const main = displays.find(d => d.isMainDisplay);
capture.captureDisplay(main.displayId);
---
##### [12] captureMultipleApps(appIdentifiers, options?): boolean
Capture from multiple apps simultaneously. Audio is mixed into a single stream.
| Parameter | Type | Description |
|-----------|------|-------------|
| appIdentifiers | Array | App names, PIDs, bundle IDs, or ApplicationInfo objects |
| Additional Option | Type | Default | Description |
|-------------------|------|---------|-------------|
| allowPartial | boolean | false | Continue if some apps not found |
`typescript`
// Capture game + Discord audio
capture.captureMultipleApps(['Minecraft', 'Discord'], {
allowPartial: true, // Continue even if one app not found
format: 'int16'
});
---
##### [13] captureMultipleWindows(windowIdentifiers, options?): boolean
Capture from multiple windows. Audio is mixed.
| Parameter | Type | Description |
|-----------|------|-------------|
| windowIdentifiers | Array | Window IDs or WindowInfo objects |
| Additional Option | Type | Default | Description |
|-------------------|------|---------|-------------|
| allowPartial | boolean | false | Continue if some windows not found |
`typescript`
const windows = capture.getWindows({ requireTitle: true });
const browserWindows = windows.filter(w => /Safari|Chrome/.test(w.owningApplicationName));
capture.captureMultipleWindows(browserWindows.map(w => w.windowId));
---
##### [14] captureMultipleDisplays(displayIdentifiers, options?): boolean
Capture from multiple displays. Audio is mixed.
| Parameter | Type | Description |
|-----------|------|-------------|
| displayIdentifiers | Array | Display IDs or DisplayInfo objects |
| Additional Option | Type | Default | Description |
|-------------------|------|---------|-------------|
| allowPartial | boolean | false | Continue if some displays not found |
`typescript`
const displays = capture.getDisplays();
capture.captureMultipleDisplays(displays.map(d => d.displayId));
---
##### [15] stopCapture(): void
Stop the current capture session. Emits 'stop' event.
---
##### [16] isCapturing(): boolean
Check if currently capturing.
`typescript`
if (capture.isCapturing()) {
capture.stopCapture();
}
---
##### [17] getStatus(): CaptureStatus | null
Get detailed capture status. Returns null if not capturing.
Returns CaptureStatus:
- capturing: Always true when not nullprocessId
- : Process ID (may be null for display capture)app
- : ApplicationInfo or nullwindow
- : WindowInfo or nulldisplay
- : DisplayInfo or nulltargetType
- : 'application' | 'window' | 'display' | 'multi-app'config
- : { minVolume, format }
`typescriptType: ${status.targetType}, App: ${status.app?.applicationName}
const status = capture.getStatus();
if (status) {
console.log();`
}
---
##### [18] getCurrentCapture(): CaptureInfo | null
Get current capture target info. Same as getStatus() but without config.
---
#### Stream Methods
##### [19] createAudioStream(appIdentifier, options?): AudioStream
Create a Node.js Readable stream for audio capture.
| Parameter | Type | Description |
|-----------|------|-------------|
| appIdentifier | string \| number | App name, bundle ID, or PID |
| Additional Option | Type | Default | Description |
|-------------------|------|---------|-------------|
| objectMode | boolean | false | Emit AudioSample objects instead of Buffers |
`typescript
// Raw buffer mode (for piping)
const stream = capture.createAudioStream('Spotify');
stream.pipe(myWritable);
// Object mode (for metadata access)
const stream = capture.createAudioStream('Spotify', { objectMode: true });
stream.on('data', (sample) => console.log(RMS: ${sample.rms}));
// Stop stream
stream.stop();
`
---
##### [20] createSTTStream(appIdentifier?, options?): STTConverter
Create stream pre-configured for Speech-to-Text engines.
| Parameter | Type | Description |
|-----------|------|-------------|
| appIdentifier | string \| number \| Array \| null | App identifier(s), null for auto-select |
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| format | 'int16' \| 'float32' | 'int16' | Output format |channels
| | 1 \| 2 | 1 | Output channels (mono recommended) |objectMode
| | boolean | false | Emit objects with metadata |autoSelect
| | boolean | true | Auto-select first audio app if not found |minVolume
| | number | - | Silence filter threshold |
`typescript
// Auto-selects first audio app, converts to Int16 mono
const sttStream = capture.createSTTStream();
sttStream.pipe(yourSTTEngine);
// With fallback apps
const sttStream = capture.createSTTStream(['Zoom', 'Safari', 'Chrome']);
// Access selected app
console.log(Selected: ${sttStream.app.applicationName});
// Stop
sttStream.stop();
`
---
#### Activity Tracking Methods
##### [21] enableActivityTracking(options?): void
Enable background tracking of audio activity. Useful for sorting apps by recent audio.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| decayMs | number | 30000 | Remove apps from cache after this many ms of inactivity |
`typescript`
capture.enableActivityTracking({ decayMs: 60000 }); // 60s decay
---
##### [22] disableActivityTracking(): void
Disable tracking and clear the cache.
`typescript`
capture.disableActivityTracking();
---
##### [23] getActivityInfo(): ActivityInfo
Get activity tracking status and statistics.
Returns ActivityInfo:
- enabled: Whether tracking is enabledtrackedApps
- : Number of apps in cacherecentApps
- : Array of ProcessActivityInfo:processId
- : Process IDlastSeen
- : Timestamp of last audioageMs
- : Time since last audioavgRMS
- : Average RMS levelsampleCount
- : Number of samples received
`typescriptActive apps: ${info.trackedApps}
const info = capture.getActivityInfo();
console.log();PID ${app.processId}: ${app.sampleCount} samples
info.recentApps.forEach(app => {
console.log();`
});
---
#### Lifecycle Methods
##### [24] dispose(): void
Release all resources and stop any active capture. Safe to call multiple times (idempotent).
`typescript
const capture = new AudioCapture();
capture.startCapture('Spotify');
// When done, release resources
capture.dispose();
// Instance can no longer be used
console.log(capture.isDisposed()); // true
`
---
##### [25] isDisposed(): boolean
Check if this instance has been disposed.
`typescript`
if (!capture.isDisposed()) {
capture.startCapture('Spotify');
}
> Note: Calling methods like startCapture(), captureWindow(), or captureDisplay() on a disposed instance will throw an error.
---
##### [S1] AudioCapture.verifyPermissions(): PermissionStatus
Check screen recording permission before capture.
Returns PermissionStatus:
- granted: Whether permission is grantedmessage
- : Human-readable statusapps
- : Prefetched app list (reuse with selectApp({ appList }))availableApps
- : Number of apps foundremediation
- : Fix instructions (if not granted)
`typescript
const status = AudioCapture.verifyPermissions();
if (!status.granted) {
console.error(status.message);
console.log(status.remediation);
process.exit(1);
}
// Reuse apps list
const app = capture.selectApp(['Spotify'], { appList: status.apps });
`
---
##### [S2] AudioCapture.bufferToFloat32Array(buffer): Float32Array
Convert Buffer to Float32Array for audio processing.
`typescript`
capture.on('audio', (sample) => {
const floats = AudioCapture.bufferToFloat32Array(sample.data);
// Process individual samples
for (let i = 0; i < floats.length; i++) {
const value = floats[i]; // Range: -1.0 to 1.0
}
});
---
##### [S3] AudioCapture.rmsToDb(rms): number
Convert RMS value (0-1) to decibels.
`typescript`
const db = AudioCapture.rmsToDb(0.5); // -6.02 dB
const db = AudioCapture.rmsToDb(sample.rms);
---
##### [S4] AudioCapture.peakToDb(peak): number
Convert peak value (0-1) to decibels.
`typescript`
const db = AudioCapture.peakToDb(sample.peak);
---
##### [S5] AudioCapture.calculateDb(buffer, method?): number
Calculate dB level directly from audio buffer.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| buffer | Buffer | - | Audio data buffer |method
| | 'rms' \| 'peak' | 'rms' | Calculation method |
`typescript`
capture.on('audio', (sample) => {
const rmsDb = AudioCapture.calculateDb(sample.data, 'rms');
const peakDb = AudioCapture.calculateDb(sample.data, 'peak');
});
---
##### [S6] AudioCapture.writeWav(buffer, options): Buffer
Create a complete WAV file from PCM audio data.
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| sampleRate | number | ā | Sample rate in Hz |channels
| | number | ā | Number of channels |format
| | 'float32' \| 'int16' | | Audio format (default: 'float32') |
`typescript
import fs from 'fs';
capture.on('audio', (sample) => {
const wav = AudioCapture.writeWav(sample.data, {
sampleRate: sample.sampleRate,
channels: sample.channels,
format: sample.format
});
fs.writeFileSync('output.wav', wav);
});
`
---
##### [S7] AudioCapture.cleanupAll(): number
Dispose all active AudioCapture instances. Returns the number of instances cleaned up.
`typescript
// Create multiple instances
const capture1 = new AudioCapture();
const capture2 = new AudioCapture();
console.log(AudioCapture.getActiveInstanceCount()); // 2
// Clean up all at once
const cleaned = AudioCapture.cleanupAll();
console.log(Cleaned up ${cleaned} instances); // 2`
---
##### [S8] AudioCapture.getActiveInstanceCount(): number
Get the number of active (non-disposed) AudioCapture instances.
`typescript
const capture = new AudioCapture();
console.log(AudioCapture.getActiveInstanceCount()); // 1
capture.dispose();
console.log(AudioCapture.getActiveInstanceCount()); // 0
`
---
#### Class: AudioCaptureError
Custom error class thrown by the SDK.
- message: Human-readable error messagecode
- : Machine-readable error code (see below)details
- : Additional context (e.g., processId, availableApps)
#### Error Codes
Import ErrorCode for reliable error checking:
`typescript
import { AudioCapture, AudioCaptureError, ErrorCode } from 'screencapturekit-audio-capture';
const capture = new AudioCapture();
capture.on('error', (err: AudioCaptureError) => {
if (err.code === ErrorCode.APP_NOT_FOUND) {
// Handle missing app
}
});
`
| Code | Description |
|------|-------------|
| ERR_PERMISSION_DENIED | Screen Recording permission not granted |ERR_APP_NOT_FOUND
| | Application not found by name or bundle ID |ERR_PROCESS_NOT_FOUND
| | Process ID not found or not running |ERR_ALREADY_CAPTURING
| | Attempted to start capture while already capturing |ERR_CAPTURE_FAILED
| | Native capture failed to start (e.g., app has no windows) |ERR_INVALID_ARGUMENT
| | Invalid arguments provided to method |
Using Error Codes:
`typescript
import { AudioCapture, AudioCaptureError, ErrorCode } from 'screencapturekit-audio-capture';
const capture = new AudioCapture();
capture.on('error', (err: AudioCaptureError) => {
switch (err.code) {
case ErrorCode.PERMISSION_DENIED:
console.log('Grant Screen Recording permission');
break;
case ErrorCode.APP_NOT_FOUND:
console.log('App not found:', err.details.requestedApp);
console.log('Available:', err.details.availableApps);
break;
case ErrorCode.ALREADY_CAPTURING:
console.log('Stop current capture first');
capture.stopCapture();
break;
default:
console.error('Error:', err.message);
}
});
`
---
AudioStream - Readable stream extending Node.js Readable:stop()
- - Stop stream and capturegetCurrentCapture()
- - Get current capture info
STTConverter - Transform stream extending Node.js Transform:stop()
- - Stop stream and captureapp
- - The selected ApplicationInfocaptureOptions
- - Options used for capture
---
For advanced users who need direct access to the native binding:
`typescript
import { ScreenCaptureKit } from 'screencapturekit-audio-capture';
const captureKit = new ScreenCaptureKit();
// Get apps (returns basic ApplicationInfo array)
const apps = captureKit.getAvailableApps();
// Start capture (requires manual callback handling)
captureKit.startCapture(processId, config, (sample) => {
// sample: { data, sampleRate, channelCount, timestamp }
// No enhancement - raw native data
});
captureKit.stopCapture();
const isCapturing = captureKit.isCapturing();
`
When to use:
- Absolute minimal overhead needed
- Building your own wrapper
- Avoiding event emitter overhead
Most users should use AudioCapture instead.
---
macOS ScreenCaptureKit only allows one process to capture audio at a time. If you need multiple processes to receive the same audio data, use the server/client architecture.
> š See readme_examples/advanced/20-capture-service.ts for a complete example
#### When to Use
| Scenario | Solution |
|----------|----------|
| Single app capturing audio | Use AudioCapture directly |AudioCaptureServer
| Multiple processes need same audio | Use + AudioCaptureClient |
| Electron main + renderer processes | Use server/client |
| Microservices architecture | Use server/client |
#### Architecture Overview
``
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā AudioCaptureServer ā
ā - Runs in one process ā
ā - Handles actual ScreenCaptureKit capture ā
ā - Broadcasts audio to all connected clients ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā WebSocket (ws://localhost:9123)
ā¼
āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā
ā Client 1 ā ā Client 2 ā ā Client N ā
ā (Process A)ā ā (Process B)ā ā (Process N)ā
āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā āāāāāāāāāāāāāāā
#### Server Usage
`typescript
import { AudioCaptureServer } from 'screencapturekit-audio-capture';
const server = new AudioCaptureServer({
port: 9123, // Default: 9123
host: 'localhost' // Default: 'localhost'
});
// Start the server
await server.start();
// Server events
server.on('clientConnected', (clientId) => console.log(Client ${clientId} connected));Client ${clientId} disconnected
server.on('clientDisconnected', (clientId) => console.log());Capture started: ${session.id}
server.on('captureStarted', (session) => console.log());
server.on('captureStopped', () => console.log('Capture stopped'));
server.on('captureError', (error) => console.error('Capture error:', error));
// Stop the server
await server.stop();
`
#### Server Methods
| Method | Returns | Description |
|--------|---------|-------------|
| start() | Promise | Start the WebSocket server |stop()
| | Promise | Stop server and disconnect all clients |getSession()
| | CaptureSession \| null | Get current capture session info |getClientCount()
| | number | Get number of connected clients |
#### Server Events
| Event | Payload | Description |
|-------|---------|-------------|
| 'clientConnected' | clientId: string | Client connected |'clientDisconnected'
| | clientId: string | Client disconnected |'captureStarted'
| | CaptureSession | Capture session started |'captureStopped'
| | - | Capture session stopped |'captureError'
| | Error | Capture error occurred |
#### Client Usage
`typescript
import { AudioCaptureClient } from 'screencapturekit-audio-capture';
const client = new AudioCaptureClient({
url: 'ws://localhost:9123', // Default
autoReconnect: true, // Default: true
reconnectDelay: 1000, // Default: 1000ms
maxReconnectAttempts: 10 // Default: 10
});
// Connect to server
await client.connect();
// Receive audio (similar API to AudioCapture)
client.on('audio', (sample) => {
console.log(Received ${sample.data.length} samples at ${sample.sampleRate}Hz);
});
// List available apps (via server)
const apps = await client.getApplications();
// Start capture (request sent to server)
await client.startCapture('Spotify');
// Or by PID: await client.startCapture(12345);
// Other capture methods
await client.captureWindow(windowId);
await client.captureDisplay(displayId);
await client.captureMultipleApps(['Spotify', 'Discord']);
// Get server status
const status = await client.getStatus();
console.log(Capturing: ${status.capturing}, Clients: ${status.totalClients});
// Stop capture
await client.stopCapture();
// Disconnect
client.disconnect();
`
#### Client Methods
| Method | Returns | Description |
|--------|---------|-------------|
| connect() | Promise | Connect to the server |disconnect()
| | void | Disconnect from server |getApplications()
| | Promise | List apps via server |getWindows()
| | Promise | List windows via server |getDisplays()
| | Promise | List displays via server |startCapture(target, opts?)
| | Promise | Start app capture |captureWindow(id, opts?)
| | Promise | Start window capture |captureDisplay(id, opts?)
| | Promise | Start display capture |captureMultipleApps(targets, opts?)
| | Promise | Start multi-app capture |stopCapture()
| | Promise | Stop current capture |getStatus()
| | Promise | Get server status |getClientId()
| | string \| null | Get this client's ID |getSessionId()
| | string \| null | Get current session ID |
#### Client Events
| Event | Payload | Description |
|-------|---------|-------------|
| 'connected' | - | Connected to server |'disconnected'
| | - | Disconnected from server |'reconnecting'
| | attempt: number | Attempting to reconnect |'reconnectFailed'
| | - | Max reconnect attempts reached |'audio'
| | RemoteAudioSample | Audio data received |'captureStopped'
| | - | Server stopped capture |'captureError'
| | { message: string } | Server capture error |'error'
| | Error | Client-side error |
#### RemoteAudioSample
Audio samples received by clients have this structure:
| Property | Type | Description |
|----------|------|-------------|
| data | Float32Array | Audio sample data |sampleRate
| | number | Sample rate in Hz |channels
| | number | Number of channels |timestamp
| | number | Timestamp in seconds |
---
Emitted when capture starts.
`typescriptCapturing from ${app?.applicationName}
capture.on('start', ({ processId, app }) => {
console.log();`
});
Emitted for each audio sample. See Audio Sample Structure for all properties.
`typescript${sample.durationMs}ms, RMS: ${sample.rms}
capture.on('audio', (sample: AudioSample) => {
console.log();`
});
Emitted when capture stops.
`typescript`
capture.on('stop', ({ processId }) => {
console.log('Capture stopped');
});
Emitted on errors.
`typescript[${err.code}]:
capture.on('error', (err: AudioCaptureError) => {
console.error(, err.message);`
});
---
Full type definitions included. See Module Exports for import syntax.
| Type | Description |
|------|-------------|
| AudioSample | Audio sample with data and metadata |ApplicationInfo
| | App info (processId, bundleIdentifier, applicationName) |WindowInfo
| | Window info (windowId, title, frame, etc.) |DisplayInfo
| | Display info (displayId, width, height, etc.) |CaptureInfo
| | Current capture target info |CaptureStatus
| | Full capture status including config |PermissionStatus
| | Permission verification result |ActivityInfo
| | Activity tracking stats |CaptureOptions
| | Options for startCapture() |AudioStreamOptions
| | Options for createAudioStream() |STTStreamOptions
| | Options for createSTTStream() |MultiAppCaptureOptions
| | Options for captureMultipleApps() |MultiWindowCaptureOptions
| | Options for captureMultipleWindows() |MultiDisplayCaptureOptions
| | Options for captureMultipleDisplays() |ServerOptions
| | Options for AudioCaptureServer |ClientOptions
| | Options for AudioCaptureClient |RemoteAudioSample
| | Audio sample received via client |CleanupResult
| | Result of cleanupAll() operation |ErrorCode
| | Enum of error codes |
---
Audio samples are Node.js Buffer objects containing Float32 PCM by default:
`typescript`
capture.on('audio', (sample) => {
// Use helper (recommended)
const float32 = AudioCapture.bufferToFloat32Array(sample.data);
// Or manual
const float32Manual = new Float32Array(
sample.data.buffer,
sample.data.byteOffset,
sample.data.byteLength / 4
);
});
`typescript
capture.startCapture('Spotify', { format: 'int16' });
capture.on('audio', (sample) => {
const int16 = new Int16Array(
sample.data.buffer,
sample.data.byteOffset,
sample.data.byteLength / 2
);
});
`
`typescript`
capture.startCapture('Spotify', { minVolume: 0.01 });
// Only emits audio events when volume > 0.01 RMS
---
> š See readme_examples/advanced/21-graceful-cleanup.ts for a complete example
Properly managing resources ensures your application shuts down cleanly without orphaned captures or memory leaks.
`typescript
const capture = new AudioCapture();
capture.startCapture('Spotify');
// When done with this specific instance
capture.dispose(); // Stops capture and releases resources
`
`typescript
import { cleanupAll, getActiveInstanceCount, installGracefulShutdown } from 'screencapturekit-audio-capture';
// Check active instances
console.log(Active: ${getActiveInstanceCount()});
// Clean up all instances at once
const result = await cleanupAll(); // Returns CleanupResult
console.log(Cleaned up ${result.total} instances);
// Install automatic cleanup on process exit (SIGINT, SIGTERM, etc.)
installGracefulShutdown();
`
| Pattern | When to Use |
|---------|-------------|
| capture.dispose() | Cleaning up a specific instance |AudioCapture.cleanupAll()
| | Cleaning up all AudioCapture instances |cleanupAll()
| | Cleaning up all instances (AudioCapture + AudioCaptureServer) |installGracefulShutdown()
| | Auto-cleanup on Ctrl+C, kill signals, or uncaught exceptions |
Exit handlers are automatically installed when you create an AudioCapture or AudioCaptureServer instance. For explicit control:
`typescript
import { installGracefulShutdown } from 'screencapturekit-audio-capture';
// Install once at application startup
installGracefulShutdown();
// Now SIGINT/SIGTERM will automatically:
// 1. Stop all active captures
// 2. Dispose all instances
// 3. Exit cleanly
`
---
Solution: Grant Screen Recording permission in System Preferences ā Privacy & Security ā Screen Recording, then restart your terminal.
Solutions:
1. Check if the app is running
2. Use capture.getApplications() to list available appscapture.startCapture('com.spotify.client')
3. Use bundle ID instead of name:
Solutions:
1. Ensure the app is playing audio
2. Check if audio is muted
3. Remove minVolume threshold for testing
4. Verify the app has visible windows
> Note: Most users won't see build errors since prebuilt binaries are included. These steps apply only if compilation is needed.
Solutions:
1. Install Xcode CLI Tools: xcode-select --installsw_vers
2. Verify macOS 13.0+: npm run clean && npm run build
3. Clean rebuild:
---
> š All examples are in readme_examples/
| Example | File | Description |
|---------|------|-------------|
| Quick Start | basics/01-quick-start.ts | Basic capture setup |
| Robust Capture | basics/05-robust-capture.ts | Production error handling |
| Find Apps | basics/11-find-apps.ts | App discovery |
| Example | File | Description |
|---------|------|-------------|
| STT Integration | voice/02-stt-integration.ts | Speech-to-text patterns |
| Voice Agent | voice/03-voice-agent.ts | Real-time voice processing |
| Recording | voice/04-audio-recording.ts | Record and save as WAV |
| Example | File | Description |
|---------|------|-------------|
| Stream Basics | streams/06-stream-basics.ts | Stream API fundamentals |
| Stream Processing | streams/07-stream-processing.ts | Transform streams |
| Example | File | Description |
|---------|------|-------------|
| Visualizer | processing/08-visualizer.ts | ASCII volume display |
| Volume Monitor | processing/09-volume-monitor.ts | Level alerts |
| Int16 Capture | processing/10-int16-capture.ts | Int16 format |
| Manual Processing | processing/12-manual-processing.ts | Buffer manipulation |
| Example | File | Description |
|---------|------|-------------|
| Multi-App Capture | capture-targets/13-multi-app-capture.ts | Multiple apps |
| Per-App Streams | capture-targets/14-per-app-streams.ts | Separate streams |
| Window Capture | capture-targets/15-window-capture.ts | Single window |
| Display Capture | capture-targets/16-display-capture.ts | Full display |
| Multi-Window | capture-targets/17-multi-window-capture.ts | Multiple windows |
| Multi-Display | capture-targets/18-multi-display-capture.ts | Multiple displays |
| Example | File | Description |
|---------|------|-------------|
| Advanced Methods | advanced/19-advanced-methods.ts | Activity tracking |
| Capture Service | advanced/20-capture-service.ts | Multi-process sharing |
| Graceful Cleanup | advanced/21-graceful-cleanup.ts | Resource lifecycle management |
Run examples:
`bash`
npx tsx readme_examples/basics/01-quick-start.ts
npm run test:readme # Run all examples
Targeting specific apps/windows/displays:
Most examples support environment variables to target specific sources instead of using defaults:
| Env Variable | Type | Used By | Example |
|-------------|------|---------|---------|
| TARGET_APP | App name | 01-12, 19-21 | TARGET_APP="Spotify" npx tsx readme_examples/basics/01-quick-start.ts |TARGET_APPS
| | Comma-separated | 13, 14 | TARGET_APPS="Safari,Music" npx tsx readme_examples/capture-targets/13-multi-app-capture.ts |TARGET_WINDOW
| | Window ID | 15, 17 | TARGET_WINDOW=12345 npx tsx readme_examples/capture-targets/15-window-capture.ts |TARGET_DISPLAY
| | Display ID | 16, 18 | TARGET_DISPLAY=1 npx tsx readme_examples/capture-targets/16-display-capture.ts |VERIFY
| | 1 or true | 13 | VERIFY=1 npx tsx readme_examples/capture-targets/13-multi-app-capture.ts |
> Tip: Run npx tsx readme_examples/basics/11-find-apps.ts to list available apps and their names. Window/display IDs are printed when running the respective capture examples.TARGET_APP="Spotify" npx tsx ...
>
> Important: Environment variables must be placed before the command, not after. works, but npx tsx ... TARGET_APP="Spotify" does not.
---
| macOS Version | Support | Notes |
|---------------|---------|-------|
| macOS 15+ (Sequoia) | ā ļø Known issues | Single-process audio capture limitation (use server/client) |
| macOS 14+ (Sonoma) | ā
Full | Recommended |
| macOS 13+ (Ventura) | ā
Full | Minimum required |
| macOS 12.x and below | ā No | ScreenCaptureKit not available |
| Windows/Linux | ā No | macOS-only framework |
> Note: On macOS 15+, only one process can capture audio at a time via ScreenCaptureKit. If you need multiple processes to receive audio, use the Multi-Process Capture Service.
---
Typical (Apple Silicon M1):
- CPU: <1% for stereo Float32
- Memory: ~10-20MB
- Latency: ~160ms (configurable)
Optimization tips:
- Use minVolume to filter silenceformat: 'int16'
- Use for 50% memory reductionchannels: 1
- Use for another 50% reduction
---
`bash``
git clone https://github.com/mrlionware/screencapturekit-audio-capture.git
cd screencapturekit-audio-capture
npm install
npm run build
npm test
---
MIT License - see LICENSE
---
Made with ā¤ļø for the Node.js and macOS developer community