
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.
native-audio-node
Advanced tools
Native audio capture for Node.js - system audio and microphone recording (macOS & Windows)
Native audio capture for Node.js - system audio and microphone recording for macOS and Windows.
A TypeScript-first library that provides low-latency access to native audio APIs for capturing both system audio output (everything playing through speakers/headphones) and microphone input.
| Feature | macOS | Windows |
|---|---|---|
| System audio capture | ✅ | ✅ |
| Microphone capture | ✅ | ✅ |
| Microphone activity monitoring | ✅ | ✅ |
| Identify processes using mic | ✅ | ✅ |
| Process filtering (include) | ✅ Multiple PIDs | ✅ Single PID* |
| Process filtering (exclude) | ✅ Multiple PIDs | ✅ Single PID* |
| Mute captured processes | ✅ | ❌ |
| Continuous audio stream | ✅ Always | ✅ Via emitSilence** |
| Sample rate conversion | ✅ | ✅ |
| Device enumeration | ✅ | ✅ |
*Windows limitation: WASAPI process loopback only supports a single process ID per capture stream.
**macOS always emits continuous audio data (silence when nothing plays). Windows WASAPI only emits when audio is playing, but emitSilence: true (default) generates silent buffers to match macOS behavior.
npm install native-audio-node
Pre-built binaries are automatically installed for your platform. No build tools required!
import { SystemAudioRecorder } from 'native-audio-node'
const recorder = new SystemAudioRecorder({
sampleRate: 16000, // Resample to 16kHz
chunkDurationMs: 100, // 100ms audio chunks
mute: false, // Don't mute system audio (macOS only)
})
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 'native-audio-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 'native-audio-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 (macOS only) |
emitSilence | boolean | true | Emit silent chunks when no audio is playing (Windows only - macOS always emits) |
includeProcesses | number[] | - | Only capture audio from these process IDs (Windows: first PID only) |
excludeProcesses | number[] | - | Exclude audio from these process IDs (Windows: first PID only) |
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 'native-audio-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 |
emitSilence | boolean | true | Emit silent chunks when no audio (Windows only - macOS always emits) |
deviceId | string | System default | Device UID (from listAudioDevices()) |
gain | number | 1.0 | Microphone gain (0.0-2.0) |
MicrophoneActivityMonitorMonitors microphone usage by any application on the system. Detects when apps start/stop using the microphone and identifies which processes are recording.
import { MicrophoneActivityMonitor } from 'native-audio-node'
const monitor = new MicrophoneActivityMonitor(options?: MicrophoneActivityMonitorOptions)
Options:
| Option | Type | Default | Description |
|---|---|---|---|
scope | 'all' | 'default' | 'all' | Monitor all input devices or only the default |
fallbackPollInterval | number | 2000 | Polling interval in ms when native events unavailable |
Methods:
| Method | Returns | Description |
|---|---|---|
start() | void | Start monitoring microphone activity |
stop() | void | Stop monitoring and release resources |
isActive() | boolean | Check if any microphone is currently in use |
isRunning() | boolean | Check if the monitor is currently running |
getActiveDevices() | AudioDevice[] | Get list of devices currently being used |
getActiveProcesses() | AudioProcess[] | Get list of processes using the microphone |
Events:
interface MicrophoneActivityMonitorEvents {
change: (isActive: boolean, processes: AudioProcess[]) => void
deviceChange: (device: AudioDevice, isActive: boolean) => void
error: (error: Error) => void
}
| Event | Payload | Description |
|---|---|---|
change | isActive, processes | Aggregate mic activity changed; includes active processes |
deviceChange | device, isActive | Specific device activity changed |
error | Error | An error occurred during monitoring |
Example:
import { MicrophoneActivityMonitor } from 'native-audio-node'
const monitor = new MicrophoneActivityMonitor()
monitor.on('change', (isActive, processes) => {
if (isActive) {
console.log('Microphone in use!')
// On macOS, processes contains apps using the mic
for (const proc of processes) {
console.log(` ${proc.name} (PID: ${proc.pid})`)
}
} else {
console.log('Microphone idle')
}
})
monitor.on('deviceChange', (device, isActive) => {
console.log(`${device.name}: ${isActive ? 'active' : 'inactive'}`)
})
monitor.start()
// Query current state anytime
console.log('Is mic active?', monitor.isActive())
console.log('Active processes:', monitor.getActiveProcesses())
// Later...
monitor.stop()
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 |
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
}
AudioProcessInformation about a process using audio input.
interface AudioProcess {
pid: number // Process ID
name: string // Process name (e.g., "Zoom", "node")
bundleId: string // macOS bundle identifier (e.g., "us.zoom.xos"), empty on Windows
}
import {
listAudioDevices,
getDefaultInputDevice,
getDefaultOutputDevice
} from 'native-audio-node'
// List all audio devices
const devices = listAudioDevices()
// 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
| Permission | macOS | Windows |
|---|---|---|
| System Audio | Requires TCC permission | No permission needed |
| Microphone | Requires user consent | May prompt via Windows Privacy |
import {
getSystemAudioPermissionStatus,
isSystemAudioPermissionAvailable,
requestSystemAudioPermission,
ensureSystemAudioPermission,
openSystemSettings,
PermissionError,
} from 'native-audio-node'
// Check current status
// macOS: Returns 'unknown', 'denied', or 'authorized'
// Windows: Always returns 'authorized'
const status = getSystemAudioPermissionStatus()
// Open system settings (macOS: Privacy pane, Windows: Sound settings)
openSystemSettings()
// Ensure permission is granted (throws PermissionError if denied on macOS)
try {
await ensureSystemAudioPermission()
// Permission granted, safe to start recording
} catch (err) {
if (err instanceof PermissionError) {
console.log('Permission status:', err.status)
}
}
import {
getMicrophonePermissionStatus,
requestMicrophonePermission,
ensureMicrophonePermission,
PermissionError,
} from 'native-audio-node'
// Check current status
const status = getMicrophonePermissionStatus()
// Request permission (shows system dialog if needed)
const granted = await requestMicrophonePermission()
// Or use convenience function
try {
await ensureMicrophonePermission()
} catch (err) {
if (err instanceof PermissionError) {
console.log('Microphone access denied')
}
}
import { SystemAudioRecorder } from 'native-audio-node'
// Record only from a specific process
const recorder = new SystemAudioRecorder({
includeProcesses: [12345], // Process ID
// Note: On Windows, only the first PID is used
})
await recorder.start()
import { MicrophoneRecorder } from 'native-audio-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()
Detect when any application uses the microphone:
import { MicrophoneActivityMonitor } from 'native-audio-node'
const monitor = new MicrophoneActivityMonitor()
monitor.on('change', (isActive, processes) => {
if (isActive) {
console.log('🎤 Microphone in use!')
// macOS: identify which apps are using the mic
if (processes.length > 0) {
console.log('Apps using mic:')
for (const proc of processes) {
console.log(` - ${proc.name} (PID: ${proc.pid})`)
}
}
} else {
console.log('🔇 Microphone idle')
}
})
monitor.start()
// Check state programmatically
setInterval(() => {
const processes = monitor.getActiveProcesses()
if (processes.length > 0) {
console.log('Currently recording:', processes.map(p => p.name).join(', '))
}
}, 5000)
TypeScript API
|
BaseAudioRecorder (EventEmitter, 10ms polling)
|
Native NAPI Wrapper (C++)
|
+--------------------+--------------------+
| macOS | Windows |
| (Swift Bridge) | (WASAPI C++) |
+--------------------+--------------------+
| |
+--------------------+--------------------+
| AudioTapManager | WasapiCapture |
| MicrophoneCapture | (Loopback/Mic) |
+--------------------+--------------------+
| |
Core Audio / AVFoundation WASAPI
MIT
FAQs
Native audio capture for Node.js - system audio and microphone recording (macOS & Windows)
The npm package native-audio-node receives a total of 60 weekly downloads. As such, native-audio-node popularity was classified as not popular.
We found that native-audio-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.