capacitor-audio-bridge
v0.1.1
Published
Capacitor plugin for native audio capture — mono, 24kHz, PCM16
Maintainers
Readme
AudioBridge
Capacitor plugin for native audio capture on iOS and Android.
Captures microphone input and normalises it to mono · 24 kHz · PCM16 little-endian, delivered as a base64 string. No networking, no transcription — raw audio only.
Supported platforms
| Platform | Status | |----------|-----------| | iOS | ✅ M1 complete | | Android | ✅ M1 complete | | Web | 🚫 Stub (throws unimplemented) |
Install
npm install capacitor-audio-bridge
npx cap synciOS setup
1. Info.plist — microphone permission
Your host app must declare a microphone usage description or iOS will crash on first permission request.
In ios/App/App/Info.plist add:
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required to record audio.</string>2. Podfile
The plugin's podspec declares AVFoundation as a framework dependency. If pod install complains, verify your Podfile includes:
platform :ios, '13.0'3. Swift version
The plugin requires Swift 5.9 (Xcode 15+). If you see a Swift compatibility error during build, ensure Xcode 15 or later is selected in Xcode → Preferences → Locations → Command Line Tools.
Android setup
1. AndroidManifest.xml
In android/app/src/main/AndroidManifest.xml add inside <manifest>:
<uses-permission android:name="android.permission.RECORD_AUDIO" />2. ProGuard / R8
If your release build uses minification, add to android/app/proguard-rules.pro:
-keepclassmembers class * {
@com.getcapacitor.annotation.PermissionCallback <methods>;
}
-keepclassmembers class * {
@com.getcapacitor.PluginMethod <methods>;
}JavaScript API
import { AudioBridge } from 'capacitor-audio-bridge';
// 0. Check permission state without prompting
const { microphone: state } = await AudioBridge.checkPermissions();
// state: 'granted' | 'denied' | 'prompt'
// 1. Request permission (shows OS sheet on first call)
const { microphone } = await AudioBridge.requestPermissions();
if (microphone !== 'granted') throw new Error('Mic denied');
// 2. Start recording
await AudioBridge.startCapture({ sampleRate: 24000, channels: 1 });
// 3. … user speaks …
// 4. Stop and get PCM payload
const result = await AudioBridge.stopCapture();
// result.dataBase64 — base64 PCM16 LE, 24 kHz, mono
// result.durationMs — recording length in milliseconds
// 5. Optional: check state mid-flow
const { value } = await AudioBridge.isCapturing();StartCaptureOptions
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| sampleRate | number | 24000 | Output Hz (always resampled to this) |
| channels | number | 1 | Output channels (always 1 / mono) |
| chunkMs | number | 100 | M2 only — ignored in M1 |
| emitChunks | boolean | false | M2 only — ignored in M1 |
StopCaptureResult
| Field | Type | Value |
|-------|------|-------|
| durationMs | number | Wall-clock length of recording |
| sampleRate | number | Always 24000 |
| channels | number | Always 1 |
| format | string | Always "pcm16" |
| dataBase64 | string | Base64-encoded PCM16 LE audio |
Error codes
All rejections include a stable code string as the second argument to reject.
| Code | Thrown by | Meaning |
|------|-----------|---------|
| PERMISSION_DENIED | startCapture | Mic permission not granted before starting |
| ALREADY_CAPTURING | startCapture | A session is already active (iOS + Android) |
| INIT_FAILED | startCapture | Native audio recorder failed to initialise (Android) |
| START_FAILED | startCapture | Engine start failed (iOS) |
| NOT_CAPTURING | stopCapture | Called while no session is active |
| STOP_FAILED | stopCapture | Error while stopping or resampling |
iOS — device validation
Prerequisites
- Physical iPhone (iOS 13+). Simulator uses the Mac microphone and behaves differently.
- Xcode 15+,
pod installcompleted, app launched with a debug build.
Steps
- Add the
NSMicrophoneUsageDescriptionkey (see setup above). - Build and deploy to device via Xcode.
- Open Xcode → Window → Devices and Simulators → your device → Open Console.
- Filter by your app's bundle ID.
- Run the JS snippet below in your ODC app.
JS test snippet (iOS)
import { AudioBridge } from 'capacitor-audio-bridge';
async function validateM1() {
const { microphone } = await AudioBridge.requestPermissions();
console.assert(microphone === 'granted', 'Permission check failed');
const { value: beforeStart } = await AudioBridge.isCapturing();
console.assert(!beforeStart, 'Should not be capturing before start');
await AudioBridge.startCapture({ sampleRate: 24000, channels: 1 });
const { value: duringCapture } = await AudioBridge.isCapturing();
console.assert(duringCapture, 'Should be capturing after start');
await new Promise(r => setTimeout(r, 3000)); // record for 3 s
const result = await AudioBridge.stopCapture();
const { value: afterStop } = await AudioBridge.isCapturing();
console.assert(!afterStop, 'Should not be capturing after stop');
console.assert(result.sampleRate === 24000, 'Wrong sample rate');
console.assert(result.channels === 1, 'Wrong channel count');
console.assert(result.format === 'pcm16', 'Wrong format');
console.assert(result.durationMs > 0, 'Zero duration');
console.assert(result.dataBase64.length > 0,'Empty payload');
// Decode and check for non-silence
const raw = atob(result.dataBase64);
let peak = 0;
for (let i = 0; i < raw.length - 1; i += 2) {
const sample = (raw.charCodeAt(i) | (raw.charCodeAt(i + 1) << 8)) << 16 >> 16;
peak = Math.max(peak, Math.abs(sample));
}
console.log(`Peak amplitude: ${peak} (0 = silent / emulator)`);
console.log(`Duration: ${result.durationMs} ms`);
console.log('✅ M1 validation passed');
}
validateM1().catch(e => console.error('❌ Validation failed:', e));Reading logs
Xcode Console will show any Swift-side errors (e.g. engineSetupFailed, permissionDenied). For more detail, use os_log output filtered by subsystem com.audiobridge.
Android — device validation
Prerequisites
- Physical Android device (API 21+). Emulator mic input is available but the level may be very low.
- Android Studio or
adb logcatavailable.
Steps
- Add
RECORD_AUDIOpermission to AndroidManifest.xml. - Build a debug APK and install on device.
- Run
adb logcat -s AudioBridge:Dto filter plugin logs. - Run the JS test snippet (same as iOS snippet above — the API is identical).
Reading logs
AudioCaptureManager emits tagged logcat lines on errors. Key tags:
AudioBridge/perm— permission stateAudioBridge/start— engine start and native sample rateAudioBridge/stop— chunk count, byte count, final duration
Device variance notes
iOS
| Scenario | Behaviour |
|----------|-----------|
| iPhone native sample rate | Hardware runs at 48 kHz. AVAudioConverter resamples to 24 kHz internally. No manual interpolation needed. |
| AirPods / Bluetooth headset | If AirPods disconnect mid-recording, handleRouteChange(.oldDeviceUnavailable) fires; the engine restarts on the built-in mic. Recording continues. |
| AirPods codec switch | Triggers AVAudioEngineConfigurationChangeNotification. Plugin reinstalls tap with the new hardware format and restarts. Unlikely during M1 flows. |
| Phone call / Siri | interruptionNotification(.began) pauses the engine. On .ended with .shouldResume, recording resumes. If the OS does not set .shouldResume (e.g. Siri redirected audio), the engine stays paused; stopCapture returns partial data. |
| iOS Simulator | Uses the Mac's default microphone. Expect low or no amplitude on machines without a mic. |
| MDM enterprise restrictions | MDM profiles can block microphone access at the OS level. requestPermissions will return "denied" regardless of the in-app prompt. |
| Host app AVAudioSession conflict | If another part of the app sets a different session category, it may conflict. AudioBridge sets .record / .measurement on startCapture. Ensure your host app does not reset the category after that point. |
Android
| Scenario | Behaviour |
|----------|-----------|
| Native sample rate | AudioRecord probes 24000 → 44100 → 48000 → 16000 Hz. Plugin resamples at stop time using linear interpolation. |
| Bluetooth SCO headset | Android may not expose the SCO mic as the default AudioSource.VOICE_RECOGNITION source without additional AudioManager.startBluetoothSco() setup. Not implemented in M1 — falls back to built-in mic. |
| Emulator | Microphone is virtualised; amplitude may be near zero or pure silence. Use a physical device for amplitude validation. |
| AGC / NS vendor processing | VOICE_RECOGNITION source disables AGC and noise suppression on most devices, but some OEM ROMs (Samsung, Xiaomi) override this. PCM data may still be processed. |
| API level < 29 | VOICE_RECOGNITION is available from API 1. No compatibility concerns for M1. |
Payload inspection utility
src/debug.ts contains inspectPayload(result) and logPayloadSummary(result) helpers for local validation. These are not exported from index.ts — import directly during development:
import { inspectPayload, logPayloadSummary } from 'capacitor-audio-bridge/src/debug';
const result = await AudioBridge.stopCapture();
logPayloadSummary(result); // logs peak, RMS, duration driftMilestone 2 (streaming)
M2 adds live audioChunk events emitted every chunkMs milliseconds. It is not yet implemented.
// Future M2 usage:
const handle = await AudioBridge.addListener('audioChunk', (event) => {
// event.dataBase64 — PCM16 LE chunk
// event.sequence — monotonic chunk index
// event.timestampMs — ms since capture started
});
await AudioBridge.startCapture({ emitChunks: true, chunkMs: 100 });Licence
MIT
