Package Exports
- capacitor-audio-engine
Readme
Capacitor Audio Engine 🎙️
Hey there! 👋 Welcome to the Native Audio plugin for Capacitor. This plugin makes it super easy to add high-quality audio recording to your mobile apps. Whether you're building a voice memo app, a podcast recorder, or just need to capture some audio, we've got you covered!
📑 Table of Contents
- Capacitor Audio Engine 🎙️
✨ Features
🎙️ Audio Recording
- 🎯 Record high-quality audio on Android and iOS
- ⏯️ Pause and resume your recordings
- 📊 Monitor recording status in real-time
- 🔒 Handle permissions automatically
- ✂️ Trim your audio files
- 📝 Get detailed recording metadata
- 🎙️ Microphone management - Detect and switch between available microphones
- 🔍 Microphone status - Check if microphone is busy/in use by other apps
🎵 Audio Playback
- 📂 Playlist support - Initialize and manage playlists of multiple audio tracks
- ▶️ Full playback controls - Play, pause, resume, stop with seamless track transitions
- ⏭️ Navigation - Skip to next/previous track or jump to specific track index
- 🎯 Seeking - Seek to any position within the current track
- 📊 Real-time info - Get current track, position, duration, and playback status
- 🔔 Event notifications - Track changes, playback state, and error handling
- 🔄 Auto-advance - Automatically play next track when current track ends
- 📱 Lock screen controls - Native media controls on iOS and Android
- 🔊 Background playback - Continue playing when app is backgrounded
- 🎨 Metadata support - Display track title, artist, and artwork
- ⚡ Audio preloading - Preload audio files for faster playback start times
- 🎼 Multi-audio resume - Resume any of multiple audio files with custom settings
- 📋 Audio information - Get detailed metadata from local and remote audio files
- 📡 Real-time monitoring - Track playback progress and status changes
- 🌐 Cross-platform support (Web coming soon!)
- 🎚️ Consistent audio quality:
- Sample Rate: 44.1kHz
- Channels: 1 (mono)
- Bitrate: 128kbps
📱 Platform Support
| Feature | Android | iOS | Web |
|---|---|---|---|
| Recording | ✅ | ✅ | 🔜 |
| Pause/Resume | ✅ | ✅ | 🔜 |
| Permission Handling | ✅ | ✅ | 🔜 |
| Status Monitoring | ✅ | ✅ | 🔜 |
| Audio Trimming | ✅ | ✅ | 🔜 |
| Microphone Detection | ✅ | ✅ | 🔜 |
| Microphone Switching | ✅ | ✅ | 🔜 |
| Audio Playback | ✅ | ✅ | 🔜 |
| Playback Controls | ✅ | ✅ | 🔜 |
| Audio Preloading | ✅ | ✅ | ❌ |
| Multi-Audio Resume | ✅ | ✅ | ❌ |
| Audio Information | ✅ | ✅ | 🔜 |
💡 Note: Android and iOS are fully supported! Web support is coming soon - we're working on it! 🚧
🚀 Installation
Prerequisites
- Node.js 14+ and npm
- Capacitor 5.0.0+
- iOS 13+ for iOS development
- Android 10+ (API level 29) for Android development
Setup
- Install the plugin:
NPM:
npm i capacitor-audio-enginePNPM:
pnpm add capacitor-audio-engineYARN
yarn add capacitor-audio-engine- Sync your project:
npx cap sync- Add required permissions:
iOS
Add these to your Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone to record audio</string>Android
Add this to your AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO" />📖 API Documentation
Core Interfaces
AudioFileInfo
export interface AudioFileInfo {
path: string;
webPath: string;
uri: string;
mimeType: string;
size: number;
duration: number;
sampleRate: number;
channels: number;
bitrate: number;
createdAt: number;
filename: string;
/**
* Base64-encoded audio data with MIME prefix (Data URI format)
* Format: "data:audio/m4a;base64,<base64-data>"
*/
base64?: string;
}RecordingOptions
export interface RecordingOptions {
/**
* Audio sample rate (Hz). Default: 44100
*/
sampleRate?: number;
/**
* Number of audio channels. Default: 1 (mono)
*/
channels?: number;
/**
* Audio bitrate (bps). Default: 128000
*/
bitrate?: number;
/**
* Note: The audio format is always .m4a (MPEG-4/AAC) on all platforms.
*/
}RecordingStatus
type RecordingStatus = 'idle' | 'recording' | 'paused';AudioRecordingEventName
type AudioRecordingEventName = 'durationChange' | 'error';DurationChangeData
export interface DurationChangeData {
duration: number;
}ErrorEventData
export interface ErrorEventData {
message: string;
code?: string | number;
details?: any;
}MicrophoneInfo
export interface MicrophoneInfo {
id: number;
name: string;
type: 'internal' | 'external' | 'unknown';
description?: string;
uid?: string; // iOS only
isConnected?: boolean; // Android only
}MicrophoneStatusResult
export interface MicrophoneStatusResult {
busy: boolean;
reason?: string;
}AvailableMicrophonesResult
export interface AvailableMicrophonesResult {
microphones: MicrophoneInfo[];
}SwitchMicrophoneOptions
export interface SwitchMicrophoneOptions {
microphoneId: number;
}SwitchMicrophoneResult
export interface SwitchMicrophoneResult {
success: boolean;
microphoneId: number;
}PlaybackStatus
type PlaybackStatus = 'idle' | 'loaded' | 'playing' | 'paused' | 'stopped' | 'completed' | 'error';PlaybackOptions
export interface PlaybackOptions {
/**
* Playback speed (0.5 - 2.0). Default: 1.0
*/
speed?: number;
/**
* Start time in seconds. Default: 0
*/
startTime?: number;
/**
* Whether to loop the audio. Default: false
*/
loop?: boolean;
/**
* Volume level (0.0 - 1.0). Default: 1.0
*/
volume?: number;
}ResumePlaybackOptions
export interface ResumePlaybackOptions {
/**
* URI of the audio file to resume
* If not provided, resumes the currently paused playback
*/
uri?: string;
/**
* Playback speed (0.5 - 2.0). Default: 1.0
*/
speed?: number;
/**
* Volume level (0.0 - 1.0). Default: 1.0
*/
volume?: number;
/**
* Whether to loop the audio. Default: false
*/
loop?: boolean;
}PreloadOptions
export interface PreloadOptions {
/**
* URI of the audio file to preload
*/
uri: string;
/**
* Whether to prepare for playback immediately. Default: true
*/
prepare?: boolean;
}AudioPlayerInfo
export interface AudioPlayerInfo {
status: PlaybackStatus;
currentTime: number;
duration: number;
speed?: number;
volume?: number;
isLooping?: boolean;
uri?: string;
}AudioPlaybackEventName
type AudioPlaybackEventName = 'playbackStatusChange' | 'playbackProgress' | 'playbackCompleted' | 'playbackError';PlaybackProgressData
export interface PlaybackProgressData {
currentTime: number;
duration: number;
position: number; // Playback position as percentage (0-100)
}PlaybackStatusData
export interface PlaybackStatusData {
status: PlaybackStatus;
currentTime?: number;
duration?: number;
}PlaybackErrorData
export interface PlaybackErrorData {
message: string;
code?: string | number;
details?: any;
}PlaybackCompletedData
export interface PlaybackCompletedData {
duration: number;
}GetAudioInfoOptions
export interface GetAudioInfoOptions {
/**
* URI of the audio file to analyze
* Supports:
* - Local file URIs (from stopRecording)
* - Remote CDN URLs (HTTP/HTTPS)
*/
uri: string;
}Methods
Permission Management
checkPermission()
Check if your app has permission to use the microphone.
checkPermission(): Promise<{ granted: boolean; audioPermission?: boolean; notificationPermission?: boolean }>;requestPermission()
Ask the user for microphone permission.
requestPermission(): Promise<{ granted: boolean; audioPermission?: boolean; notificationPermission?: boolean }>;Recording Control
startRecording()
Start recording audio from the device's microphone.
startRecording(options?: RecordingOptions): Promise<void>;pauseRecording()
Pause the current recording.
pauseRecording(): Promise<void>;resumeRecording()
Resume the current recording if it was previously paused.
resumeRecording(): Promise<void>;stopRecording()
Stop the current recording and get the recorded file information.
stopRecording(): Promise<AudioFileInfo>;Status & Information
getDuration()
Get the current recording duration.
getDuration(): Promise<{ duration: number }>;getStatus()
Check the current recording status.
getStatus(): Promise<{ status: RecordingStatus; isRecording: boolean }>;Audio Processing
trimAudio()
Trim an audio file to a specific duration.
trimAudio(options: { uri: string; start: number; end: number }): Promise<AudioFileInfo>;getAudioInfo()
Get detailed information about an audio file (local or remote).
getAudioInfo(options: GetAudioInfoOptions): Promise<AudioFileInfo>;Example:
// Get info for a local recording
const localInfo = await CapacitorAudioEngine.getAudioInfo({
uri: 'file:///path/to/recording.m4a',
});
// Get info for a remote CDN file
const remoteInfo = await CapacitorAudioEngine.getAudioInfo({
uri: 'https://example.com/audio/sample.mp3',
});
console.log('Duration:', localInfo.duration, 'seconds');
console.log('File size:', localInfo.size, 'bytes');
console.log('Sample rate:', localInfo.sampleRate, 'Hz');Platform Notes:
- Android: Uses MediaMetadataRetriever to extract metadata from local and remote files
- iOS: Uses AVAsset to extract metadata from local and remote files
- Web: Not supported
Microphone Management
isMicrophoneBusy()
Check if the microphone is currently being used by another application.
isMicrophoneBusy(): Promise<MicrophoneStatusResult>;Example:
const status = await CapacitorAudioEngine.isMicrophoneBusy();
if (status.busy) {
console.log('Microphone is busy:', status.reason);
} else {
console.log('Microphone is available');
}getAvailableMicrophones()
Get a list of available microphones (internal and external).
getAvailableMicrophones(): Promise<AvailableMicrophonesResult>;Example:
const result = await CapacitorAudioEngine.getAvailableMicrophones();
result.microphones.forEach((mic) => {
console.log(`${mic.name} (${mic.type}): ${mic.isConnected ? 'Connected' : 'Disconnected'}`);
});switchMicrophone()
Switch to a different microphone while keeping recording active.
switchMicrophone(options: SwitchMicrophoneOptions): Promise<SwitchMicrophoneResult>;Example:
// Get available microphones
const result = await CapacitorAudioEngine.getAvailableMicrophones();
const externalMic = result.microphones.find((mic) => mic.type === 'external');
if (externalMic) {
try {
const switchResult = await CapacitorAudioEngine.switchMicrophone({
microphoneId: externalMic.id,
});
console.log('Switched to:', switchResult.message);
} catch (error) {
console.error('Failed to switch microphone:', error);
}
}Platform Notes:
- Android: Shows primary built-in microphone + all external devices (headsets, USB, Bluetooth)
- iOS: Shows all available audio inputs from AVAudioSession
- Web: Not supported (returns empty array)
Audio Playback
The Audio Engine now supports comprehensive playlist-based audio playback with full control over multiple tracks.
preloadTracks()
Preload audio tracks from URLs and initialize playlist for optimal performance.
preloadTracks(options: PreloadTracksOptions): Promise<void>;
interface PreloadTracksOptions {
tracks: string[]; // Array of audio URLs
preloadNext?: boolean; // Default: true
}Example:
const trackUrls = [
'https://example.com/song1.mp3',
'file:///path/to/local/song2.m4a',
'https://example.com/song3.mp3'
];
await CapacitorAudioEngine.preloadTracks({
tracks: trackUrls,
preloadNext: true,
});playAudio()
Start playback of the current track in the playlist.
playAudio(): Promise<void>;Example:
await CapacitorAudioEngine.playAudio();pauseAudio()
Pause the current audio playback.
pauseAudio(): Promise<void>;resumeAudio()
Resume paused audio playback.
resumeAudio(): Promise<void>;stopAudio()
Stop audio playback and reset to the beginning of the current track.
stopAudio(): Promise<void>;seekAudio()
Seek to a specific position within the current track.
seekAudio(options: SeekOptions): Promise<void>;
interface SeekOptions {
seconds: number;
}Example:
// Seek to 30 seconds
await CapacitorAudioEngine.seekAudio({ seconds: 30 });skipToNext()
Skip to the next track in the playlist.
skipToNext(): Promise<void>;skipToPrevious()
Skip to the previous track in the playlist.
skipToPrevious(): Promise<void>;skipToIndex()
Jump to a specific track in the playlist by index.
skipToIndex(options: SkipToIndexOptions): Promise<void>;
interface SkipToIndexOptions {
index: number; // Zero-based track index
}Example:
// Jump to the third track (index 2)
await CapacitorAudioEngine.skipToIndex({ index: 2 });getPlaybackInfo()
Get comprehensive information about the current playback state.
getPlaybackInfo(): Promise<PlaybackInfo>;
interface PlaybackInfo {
currentTrack: AudioTrack | null;
currentIndex: number;
currentPosition: number; // in seconds
duration: number; // in seconds
isPlaying: boolean;
status: PlaybackStatus; // 'idle' | 'loading' | 'playing' | 'paused' | 'stopped'
}Example:
const info = await CapacitorAudioEngine.getPlaybackInfo();
console.log('Current track:', info.currentTrack?.title);
console.log('Position:', `${info.currentPosition}s / ${info.duration}s`);
console.log('Playing:', info.isPlaying);
console.log('Track index:', info.currentIndex);Event Handling
addListener()
Add a listener for recording or playback events.
addListener<T extends AudioEventName>(
eventName: T,
callback: (event: AudioEventMap[T]) => void,
): Promise<PluginListenerHandle>;Recording Event Examples:
// Listen for recording duration changes
await CapacitorAudioEngine.addListener('durationChange', (event) => {
console.log('Recording duration:', event.duration, 'seconds');
});
// Listen for recording errors
await CapacitorAudioEngine.addListener('error', (event) => {
console.error('Recording error:', event.message);
});Playback Event Examples:
// Listen for track changes
await CapacitorAudioEngine.addListener('trackChanged', (event) => {
console.log('Track changed:', event.track.title, 'at index', event.index);
// Update UI to show new track info
});
// Listen for track completion
await CapacitorAudioEngine.addListener('trackEnded', (event) => {
console.log('Track ended:', event.track.title);
// Track will auto-advance to next if available
});
// Listen for playback start
await CapacitorAudioEngine.addListener('playbackStarted', (event) => {
console.log('Playback started:', event.track.title);
// Update play/pause button state
});
// Listen for playback pause
await CapacitorAudioEngine.addListener('playbackPaused', (event) => {
console.log('Playback paused:', event.track.title, 'at', event.position, 'seconds');
// Update play/pause button state
});
// Listen for playback errors
await CapacitorAudioEngine.addListener('playbackError', (event) => {
console.error('Playback error:', event.message);
// Show error message to user
});
await CapacitorAudioEngine.addListener('playbackCompleted', (data) => {
console.log('Playback completed, duration:', data.duration);
});
// Listen for playback errors
await CapacitorAudioEngine.addListener('playbackError', (data) => {
console.error('Playback error:', data.message);
});removeAllListeners()
Remove all listeners for recording events.
removeAllListeners(): Promise<void>;Note: The audio format is always
.m4a(MPEG-4/AAC) on all platforms.
Usage Example
Here's a complete example of how to use the plugin with microphone management:
import { CapacitorAudioEngine } from 'capacitor-audio-engine';
class AudioRecorder {
private isRecording = false;
private availableMicrophones: MicrophoneInfo[] = [];
private selectedMicrophoneId: number | null = null;
async initialize() {
// Check and request permission
const permission = await CapacitorAudioEngine.checkPermission();
if (!permission.granted) {
const result = await CapacitorAudioEngine.requestPermission();
if (!result.granted) {
throw new Error('Microphone permission denied');
}
}
// Load available microphones
await this.loadMicrophones();
// Set up event listeners
await this.setupEventListeners();
}
async loadMicrophones() {
try {
const result = await CapacitorAudioEngine.getAvailableMicrophones();
this.availableMicrophones = result.microphones;
// Select internal microphone by default
const internalMic = result.microphones.find((mic) => mic.type === 'internal');
if (internalMic) {
this.selectedMicrophoneId = internalMic.id;
}
console.log('Available microphones:', result.microphones);
} catch (error) {
console.error('Failed to load microphones:', error);
}
}
async startRecording() {
try {
// Check if microphone is busy
const status = await CapacitorAudioEngine.isMicrophoneBusy();
if (status.busy) {
throw new Error(`Microphone is busy: ${status.reason}`);
}
// Switch to selected microphone if available
if (this.selectedMicrophoneId) {
await CapacitorAudioEngine.switchMicrophone({
microphoneId: this.selectedMicrophoneId,
});
}
// Start recording
await CapacitorAudioEngine.startRecording({
sampleRate: 44100,
channels: 1,
bitrate: 128000,
});
this.isRecording = true;
console.log('Recording started');
} catch (error) {
console.error('Failed to start recording:', error);
}
}
async stopRecording() {
try {
const result = await CapacitorAudioEngine.stopRecording();
this.isRecording = false;
console.log('Recording saved:', result);
return result;
} catch (error) {
console.error('Failed to stop recording:', error);
}
}
async playRecording(audioFile: AudioFileInfo) {
try {
// Preload the audio file for better performance
await CapacitorAudioEngine.preload({
uri: audioFile.uri,
prepare: true,
});
// Start playback with custom options
await CapacitorAudioEngine.startPlayback({
uri: audioFile.uri,
speed: 1.0, // Normal speed
volume: 1.0, // Full volume
loop: false, // Don't loop
startTime: 0, // Start from beginning
});
console.log('Playback started');
} catch (error) {
console.error('Failed to start playback:', error);
}
}
async pausePlayback() {
try {
await CapacitorAudioEngine.pausePlayback();
console.log('Playback paused');
} catch (error) {
console.error('Failed to pause playback:', error);
}
}
async resumePlayback() {
try {
// Resume current playback (existing behavior)
await CapacitorAudioEngine.resumePlayback();
console.log('Playback resumed');
} catch (error) {
console.error('Failed to resume playback:', error);
}
}
async resumePlaybackWithOptions(uri?: string, speed?: number, volume?: number, loop?: boolean) {
try {
// Resume with custom options - can switch to different audio files
await CapacitorAudioEngine.resumePlayback({
uri, // Optional: switch to different audio file
speed, // Optional: custom playback speed
volume, // Optional: custom volume level
loop, // Optional: enable/disable looping
});
console.log('Playback resumed with custom options');
} catch (error) {
console.error('Failed to resume playback with options:', error);
}
}
async stopPlayback() {
try {
await CapacitorAudioEngine.stopPlayback();
console.log('Playback stopped');
} catch (error) {
console.error('Failed to stop playback:', error);
}
}
async seekTo(time: number) {
try {
await CapacitorAudioEngine.seekTo({ time });
console.log(`Seeked to ${time} seconds`);
} catch (error) {
console.error('Failed to seek:', error);
}
}
async getPlaybackStatus() {
try {
const status = await CapacitorAudioEngine.getPlaybackStatus();
console.log('Playback status:', status);
return status;
} catch (error) {
console.error('Failed to get playback status:', error);
}
}
async switchMicrophone(microphoneId: number) {
try {
const result = await CapacitorAudioEngine.switchMicrophone({ microphoneId });
this.selectedMicrophoneId = result.microphoneId;
console.log('Switched microphone:', result.message);
} catch (error) {
console.error('Failed to switch microphone:', error);
}
}
private async setupEventListeners() {
// Recording event listeners
await CapacitorAudioEngine.addListener('durationChange', (data) => {
console.log('Recording duration:', data.duration);
});
await CapacitorAudioEngine.addListener('error', (data) => {
console.error('Recording error:', data.message);
});
// Playback event listeners
await CapacitorAudioEngine.addListener('playbackProgress', (data) => {
console.log(`Playback progress: ${data.currentTime}s / ${data.duration}s`);
});
await CapacitorAudioEngine.addListener('playbackStatusChange', (data) => {
console.log('Playback status changed to:', data.status);
});
await CapacitorAudioEngine.addListener('playbackCompleted', (data) => {
console.log('Playback completed, duration:', data.duration);
});
await CapacitorAudioEngine.addListener('playbackError', (data) => {
console.error('Playback error:', data.message);
});
}
async cleanup() {
await CapacitorAudioEngine.removeAllListeners();
}
}
// Usage
const recorder = new AudioRecorder();
await recorder.initialize();
await recorder.startRecording();
// ... record audio ...
const audioFile = await recorder.stopRecording();🔧 Troubleshooting
Common Issues
- Permission Denied
- Ensure you've added the required permissions in your platform-specific files
- Check if the user has granted permission in their device settings
- Try requesting permission again
- Recording Not Starting
- Verify that you're not already recording
- Check if the microphone is being used by another app
- Ensure you have sufficient storage space
- Audio Quality Issues
- Check if the device's microphone is working properly
- Verify that no other apps are using the microphone
- Ensure you're not in a noisy environment
- File Access Issues
- Check if the app has proper storage permissions
- Verify that the storage path is accessible
- Ensure there's enough free space
- Microphone Issues
- Use
isMicrophoneBusy()to check if another app is using the microphone - Try refreshing available microphones with
getAvailableMicrophones() - Ensure external microphones (headsets, USB) are properly connected
- On Android: Built-in microphone should always be available
- On iOS: Check if microphone access is enabled in device settings
- Use
- Microphone Switching Issues
- Verify the microphone ID exists in the available microphones list
- External microphones may disconnect during recording
- Some devices may not support seamless microphone switching during recording
🛠️ Technical Details
Platform-Specific Implementations
Web
- Uses MediaRecorder API
- Format: WebM container with Opus codec
- MIME Type: 'audio/webm;codecs=opus'
- Permission: Uses navigator.permissions.query API
- Audio trimming: Not supported (logs console message)
- Microphone Management: Not supported (returns empty arrays and placeholder responses)
Android
- Uses MediaRecorder
- Format: M4A container with AAC codec (MPEG-4/AAC, always .m4a)
- MIME Type: 'audio/m4a' or 'audio/m4a'
- Audio Source: MIC
- Storage: App's external files directory under "Recordings" folder
- Filename Format: "recording_[timestamp].m4a"
- Background Recording: Full support via foreground service with microphone type
- Required Permission:
android.permission.RECORD_AUDIO - Background Permissions:
FOREGROUND_SERVICE,FOREGROUND_SERVICE_MICROPHONE,POST_NOTIFICATIONS - Microphone Management:
- Uses AudioManager.getDevices() to enumerate input devices (API 23+)
- Shows primary built-in microphone + all external devices
- Supports headset, USB, and Bluetooth microphones
- Uses AudioRecord for microphone busy detection
- Microphone switching uses MediaRecorder.setPreferredDevice() (API 28+)
iOS
- Uses AVAudioRecorder
- Format: M4A container with AAC codec (MPEG-4/AAC, always .m4a)
- MIME Type: 'audio/m4a'
- Quality: High
- Uses AVAssetExportSession for audio trimming
- Background Recording: Supports continuous recording when app is backgrounded (requires 'audio' background mode)
- Required Permission: NSMicrophoneUsageDescription in Info.plist
- Background Mode: UIBackgroundModes with 'audio' capability
- Microphone Management:
- Uses AVAudioSession.availableInputs to list audio inputs
- Supports built-in, wired headset, and Bluetooth microphones
- Uses AVAudioSession.setPreferredInput() for microphone switching
- Real-time microphone busy detection via AVAudioSession
📚 Additional Documentation
For more detailed examples and advanced usage patterns, check out:
- Microphone Management Guide - Comprehensive guide for microphone detection, switching, and troubleshooting
🤝 Contributing
We love contributions! Whether it's fixing bugs, adding features, or improving docs, your help makes this plugin better for everyone. Here's how to help:
- Fork the repo
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
📞 Need Help?
Found a bug? Have a feature request? Just want to chat? Open an issue on GitHub and we'll help you out!
Made with ❤️ by Abdelfattah Ashour