
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
coreaudio-node
Advanced tools
Native macOS audio capture for Node.js - system audio and microphone recording
Native macOS audio capture for Node.js - system audio and microphone recording.
A TypeScript-first library that provides low-latency access to macOS Core Audio for capturing both system audio output (everything playing through speakers/headphones) and microphone input.
npm install coreaudio-node
The native addon is compiled during installation via cmake-js.
import { SystemAudioRecorder } from 'coreaudio-node'
const recorder = new SystemAudioRecorder({
sampleRate: 16000, // Resample to 16kHz
chunkDurationMs: 100, // 100ms audio chunks
mute: false, // Don't mute system audio
})
recorder.on('metadata', (meta) => {
console.log(`Format: ${meta.sampleRate}Hz, ${meta.bitsPerChannel}bit`)
})
recorder.on('data', (chunk) => {
// chunk.data is a Buffer containing raw PCM audio
console.log(`Received ${chunk.data.length} bytes`)
})
await recorder.start()
// Record for 10 seconds
setTimeout(async () => {
await recorder.stop()
}, 10000)
import { MicrophoneRecorder, listAudioDevices } from 'coreaudio-node'
// List available input devices
const devices = listAudioDevices().filter(d => d.isInput)
console.log('Available microphones:', devices.map(d => d.name))
const recorder = new MicrophoneRecorder({
sampleRate: 16000,
gain: 0.8, // 80% gain (0.0-2.0)
deviceId: devices[0].id // Optional: specific device
})
recorder.on('data', (chunk) => {
// Process microphone audio
})
await recorder.start()
SystemAudioRecorderCaptures system audio output (everything playing through speakers/headphones).
import { SystemAudioRecorder } from 'coreaudio-node'
const recorder = new SystemAudioRecorder(options?: SystemAudioRecorderOptions)
Options:
| Option | Type | Default | Description |
|---|---|---|---|
sampleRate | number | Device native | Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000) |
chunkDurationMs | number | 200 | Audio chunk duration in milliseconds (0-5000) |
stereo | boolean | false | Record in stereo (true) or mono (false) |
mute | boolean | false | Mute system audio while recording |
includeProcesses | number[] | - | Only capture audio from these process IDs |
excludeProcesses | number[] | - | Exclude audio from these process IDs |
Methods:
| Method | Returns | Description |
|---|---|---|
start() | Promise<void> | Start audio capture |
stop() | Promise<void> | Stop audio capture |
isActive() | boolean | Check if currently recording |
getMetadata() | AudioMetadata | null | Get current audio format info |
MicrophoneRecorderCaptures audio from microphone input devices.
import { MicrophoneRecorder } from 'coreaudio-node'
const recorder = new MicrophoneRecorder(options?: MicrophoneRecorderOptions)
Options:
| Option | Type | Default | Description |
|---|---|---|---|
sampleRate | number | Device native | Target sample rate |
chunkDurationMs | number | 200 | Audio chunk duration in milliseconds |
stereo | boolean | false | Record in stereo or mono |
deviceId | string | System default | Device UID (from listAudioDevices()) |
gain | number | 1.0 | Microphone gain (0.0-2.0) |
Both recorder classes emit the following events:
interface AudioRecorderEvents {
data: (chunk: AudioChunk) => void
metadata: (metadata: AudioMetadata) => void
start: () => void
stop: () => void
error: (error: Error) => void
}
| Event | Payload | Description |
|---|---|---|
data | AudioChunk | Raw PCM audio data chunk |
metadata | AudioMetadata | Audio format information (emitted once after start) |
start | - | Recording has started |
stop | - | Recording has stopped |
error | Error | An error occurred |
Usage:
recorder.on('data', (chunk) => { /* handle audio */ })
recorder.on('metadata', (meta) => { /* handle format info */ })
recorder.on('start', () => { /* recording started */ })
recorder.on('stop', () => { /* recording stopped */ })
recorder.on('error', (err) => { /* handle error */ })
// One-time listener
recorder.once('start', () => { /* only fires once */ })
// Remove listener
recorder.off('data', myHandler)
// Remove all listeners
recorder.removeAllListeners('data')
AudioChunkinterface AudioChunk {
data: Buffer // Raw PCM audio bytes
}
AudioMetadatainterface AudioMetadata {
sampleRate: number // Hz (e.g., 48000, 16000)
channelsPerFrame: number // 1 (mono) or 2 (stereo)
bitsPerChannel: number // 32 (float) or 16 (int)
isFloat: boolean // true = 32-bit float, false = 16-bit int
encoding: string // "pcm_f32le" or "pcm_s16le"
}
AudioDeviceinterface AudioDevice {
id: string // Unique device identifier
name: string // Human-readable name
manufacturer?: string // Device manufacturer
isDefault: boolean // Is system default device
isInput: boolean // Supports input (microphone)
isOutput: boolean // Supports output (speakers)
sampleRate: number // Native sample rate
channelCount: number // Number of channels
}
import {
listAudioDevices,
getDefaultInputDevice,
getDefaultOutputDevice
} from 'coreaudio-node'
// List all audio devices
const devices = listAudioDevices()
console.log(devices)
// [
// { id: 'BuiltInMicrophoneDevice', name: 'MacBook Pro Microphone', isInput: true, ... },
// { id: 'BuiltInSpeakerDevice', name: 'MacBook Pro Speakers', isOutput: true, ... },
// ...
// ]
// Get input devices only
const microphones = devices.filter(d => d.isInput)
// Get output devices only
const speakers = devices.filter(d => d.isOutput)
// Get default devices
const defaultMic = getDefaultInputDevice() // Returns device UID or null
const defaultSpeaker = getDefaultOutputDevice() // Returns device UID or null
macOS requires permission for audio recording. System audio and microphone have different permission systems.
Uses macOS TCC (Transparency, Consent, and Control) private API. Permission must often be granted manually in System Settings.
import {
getSystemAudioPermissionStatus,
isSystemAudioPermissionAvailable,
requestSystemAudioPermission,
ensureSystemAudioPermission,
openSystemSettings,
PermissionError,
} from 'coreaudio-node'
// Check if TCC API is available
if (isSystemAudioPermissionAvailable()) {
const status = getSystemAudioPermissionStatus()
// Returns: 'unknown' | 'denied' | 'authorized'
if (status !== 'authorized') {
// Open System Settings to permission pane
openSystemSettings()
console.log('Please grant "System Audio Recording Only" permission')
}
}
// Or use the convenience function (throws PermissionError if denied)
try {
await ensureSystemAudioPermission()
// Permission granted, safe to start recording
} catch (err) {
if (err instanceof PermissionError) {
console.log('Permission status:', err.status)
}
}
Uses standard AVFoundation API with system permission dialog.
import {
getMicrophonePermissionStatus,
requestMicrophonePermission,
ensureMicrophonePermission,
PermissionError,
} from 'coreaudio-node'
// Check current status
const status = getMicrophonePermissionStatus()
// Returns: 'unknown' | 'denied' | 'authorized'
// Request permission (shows system dialog if 'unknown')
const granted = await requestMicrophonePermission()
if (granted) {
// Start recording
}
// Or use convenience function
try {
await ensureMicrophonePermission()
} catch (err) {
if (err instanceof PermissionError) {
console.log('Microphone access denied')
}
}
type PermissionStatus = 'unknown' | 'denied' | 'authorized'
class PermissionError extends Error {
status: PermissionStatus
}
Audio is captured in the device's native format, typically:
When you specify a sampleRate option, audio is converted:
stereo: true)pcm_s16le (little-endian)8000, 16000, 22050, 24000, 32000, 44100, 48000 Hz
import { SystemAudioRecorder } from 'coreaudio-node'
import { writeFileSync } from 'fs'
const chunks: Buffer[] = []
let metadata: AudioMetadata
const recorder = new SystemAudioRecorder({ sampleRate: 16000 })
recorder.on('metadata', (meta) => { metadata = meta })
recorder.on('data', (chunk) => { chunks.push(chunk.data) })
await recorder.start()
// Record for 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000))
await recorder.stop()
// Combine chunks and write with WAV header
const audioData = Buffer.concat(chunks)
const wavFile = createWavFile(audioData, metadata)
writeFileSync('recording.wav', wavFile)
import { SystemAudioRecorder } from 'coreaudio-node'
import { execSync } from 'child_process'
// Get Chrome's process ID
const chromePid = parseInt(
execSync("pgrep -x 'Google Chrome'").toString().trim()
)
const recorder = new SystemAudioRecorder({
includeProcesses: [chromePid],
mute: true, // Mute Chrome audio while recording
})
await recorder.start()
import { MicrophoneRecorder } from 'coreaudio-node'
const recorder = new MicrophoneRecorder({
sampleRate: 16000,
chunkDurationMs: 50, // 50ms chunks for low latency
})
recorder.on('data', (chunk) => {
// Calculate RMS (volume level)
const samples = new Int16Array(chunk.data.buffer)
let sum = 0
for (const sample of samples) {
sum += sample * sample
}
const rms = Math.sqrt(sum / samples.length)
const db = 20 * Math.log10(rms / 32768)
console.log(`Volume: ${db.toFixed(1)} dB`)
})
await recorder.start()
import { MicrophoneRecorder, listAudioDevices } from 'coreaudio-node'
import readline from 'readline'
const devices = listAudioDevices().filter(d => d.isInput)
console.log('Select a microphone:')
devices.forEach((d, i) => {
const marker = d.isDefault ? ' (default)' : ''
console.log(` ${i + 1}. ${d.name}${marker}`)
})
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const answer = await new Promise<string>(resolve => rl.question('> ', resolve))
rl.close()
const selected = devices[parseInt(answer) - 1]
const recorder = new MicrophoneRecorder({ deviceId: selected.id })
await recorder.start()
The package includes example scripts:
# Record system audio to WAV
node examples/record-wav.mjs -o output.wav -d 10 -s 16000 -m
# Record microphone to WAV
node examples/record-mic.mjs -o mic.wav -d 5 -g 0.8
# List audio devices
node examples/record-mic.mjs -l
# Test device listing
node examples/test-devices.mjs
openSystemSettings() or open System Settings > Privacy & Security > Screen & System Audio RecordingEnsure you have Xcode Command Line Tools:
xcode-select --install
Rebuild the native addon:
npm run rebuild
The library builds a universal binary supporting both architectures. If you encounter issues:
npm run clean
npm run build:native
TypeScript API
|
BaseAudioRecorder (EventEmitter, 10ms polling)
|
Native NAPI Wrapper (C++)
|
Swift Bridge (C-compatible API)
|
+--------------------+--------------------+
| AudioTapManager | MicrophoneRecorder|
| (System Audio) | (AVCaptureSession)|
+--------------------+--------------------+
| |
macOS Core Audio / AVFoundation Frameworks
| Component | Purpose |
|---|---|
CoreAudioBridge.swift | C-compatible entry points for NAPI |
AudioTapManager.swift | System audio tapping via AudioHardwareCreateProcessTap |
MicrophoneCapture.swift | AVCaptureSession-based microphone recording |
AudioDeviceManager.swift | Device enumeration (AVCaptureDevice + Core Audio) |
AudioBuffer.swift | Ring buffer for audio chunk management |
AudioFormatConverter.swift | Sample rate conversion via AVAudioConverter |
AudioPermission.swift | TCC and AVFoundation permission handling |
# Clone the repository
git clone https://github.com/your-username/coreaudio-node.git
cd coreaudio-node
# Install dependencies
npm install
# Build native addon and TypeScript
npm run build
# Build native addon only
npm run build:native
# Build TypeScript only
npm run build:ts
# Watch mode for TypeScript
npm run dev
# Clean build artifacts
npm run clean
# Rebuild native addon
npm run rebuild
During the 0.x.x release cycle, the API is unstable and subject to change without notice.
The native addon is built locally on your machine. For distribution:
When bundled inside an Electron app, the native addon inherits the code signature from the parent application. Ensure your app's entitlements include audio recording permissions.
If running directly via Node.js:
MIT
FAQs
Native macOS audio capture for Node.js - system audio and microphone recording
The npm package coreaudio-node receives a total of 3 weekly downloads. As such, coreaudio-node popularity was classified as not popular.
We found that coreaudio-node demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.