Implementing Offline Echo-Cancelled Audio in Expo React Native

This blog provides a comprehensive guide to building an offline audio application in Expo React Native with echo cancellation using react-native-webrtc.

Blog cover image
2101050's avatar
2101050
13 views

Implementing Offline Echo-Cancelled Audio Record/Playback in Expo React Native Using react-native-webrtc


1. Introduction: The Challenge of High-Quality Offline Audio in Modern Apps

Mobile applications, including productivity tools, podcasting platforms, and secure communication suites, increasingly require robust audio functionality. Often, end users expect an app to record and play back audio reliably, even without an internet connection. More importantly, in scenarios like note-taking or two-way communication, echo cancellation becomes vital for clear voice quality.

Why Echo Cancellation and Offline Local Capability?

When replayed sound is "heard" by the microphone—such as when recording and playing audio simultaneously or when the user keeps the phone on speaker—echo is introduced. This results in feedback loops, degraded recordings, and a frustrating end-user experience. Echo cancellation is the digital signal processing (DSP) technique that minimizes this problem by subtracting predicted echo from the input.

Equally, offline capability ensures that:

  • The app functions without a network connection (crucial for global and field users).
  • User privacy and security requirements are satisfied (no cloud uploads).
  • Fast, robust performance is maintained on the device itself.

Why Use React Native (Expo) and react-native-webrtc?

  • React Native, especially with Expo, enables rapid cross-platform development and access to device APIs.
  • react-native-webrtc brings low-level, native audio and video streaming capabilities, exposing fine-grained control over audio inputs, including echo cancellation—crucial for our task.

This blog will walk step-by-step through the best practices, practical configuration, and richly annotated code to build an offline audio application in Expo React Native with echo cancellation, using both built-in features and custom native modules where necessary.

References:


2. Architectural Overview: Expo, Native Modules, and Offline Workflows

Understanding Expo Workflows

Expo offers two major development modes:

  • Managed workflow (easy, but limited for low-level modules).
  • Bare workflow (full native code and module support).

Note: Echo cancellation and advanced audio recording require direct native access; thus, you must use the Bare workflow or EAS Build with Expo modules that wrap native code such as react-native-webrtc.

react-native-webrtc: Capabilities for Local Audio

Originally designed for real-time communication (VoIP, video call), react-native-webrtc can function entirely offline if no “peer connection” is established. Its power comes from exposing:

  • Access to local device microphones and speakers at a low level.
  • Media constraints (like echoCancellation, noiseSuppression, and autoGainControl).
  • Real-time audio stream processing.
  • Direct control over audio streams and ability to save them to disk.

Key Advantages:

  • Fine-grained echo cancellation settings.
  • Compatibility with both Android and iOS native audio stacks.
  • No inherent need for networking in purely local “recorder/player” workflows.

What About expo-av or expo-audio?

  • expo-av/expo-audio provide out-of-the-box recording and playback, but do NOT expose echo cancellation and real-time audio buffer manipulation. For most offline recording, they are fine—but if echo management is crucial, you must reach for a module like react-native-webrtc.
  • Many third-party audio libraries (such as react-native-realtime-audio) attempt to abstract these advanced flows, but still require native workflow or EAS Build.

Workflow Selection Table:

RequirementExpo ManagedEAS BuildBare Workflow
Basic recording✔️✔️✔️
Echo cancellation❌✔️✔️
Native buffer access❌✔️✔️
Advanced config/control❌✔️✔️

3. Enabling Echo Cancellation in react-native-webrtc

How Echo Cancellation Works

On both iOS and Android, the WebRTC engine uses system APIs to route microphone input through hardware/OS-level echo cancellers when you request it in constraints.

In react-native-webrtc, this is controlled by passing echoCancellation: true in your media constraints to getUserMedia.

Example Media Constraints:

Js
1const mediaConstraints = { 2 audio: { 3 echoCancellation: true, 4 noiseSuppression: true, 5 autoGainControl: true, 6 sampleRate: 44100, 7 channelCount: 1 8 }, 9 video: false 10}; 11

When passed to getUserMedia, this instructs the engine to request a voice-optimized input from the OS, activating echo suppression on supported devices.

Limitations & Hardware Considerations

  • Effectiveness depends on hardware. Modern phones generally do well; older or cheap phones might not.
  • Bluetooth headsets often bypass EC hardware. Use speakerphone/mic for best results and document this for end users.
  • User permissions are essential. You must request and handle microphone permissions at runtime.

Practical Example: Requesting an Audio Stream with Echo Cancellation

Js
1import { mediaDevices } from 'react-native-webrtc'; 2 3async function getAudioStream() { 4 try { 5 const stream = await mediaDevices.getUserMedia({ 6 audio: { 7 echoCancellation: true, 8 noiseSuppression: true, 9 autoGainControl: true, 10 sampleRate: 44100, 11 channelCount: 1 12 } 13 }); 14 return stream; 15 } catch (e) { 16 console.error('Failed to get media stream:', e); 17 throw e; 18 } 19} 20

References


4. Full Example: Offline Echo-Cancelled Recorder/Player Workflow in Expo React Native

Step 1: Prerequisites and Setup

Project Initialization

Bash
1npx create-expo-app offline-echo-demo 2cd offline-echo-demo 3npx expo install react-native-webrtc 4
  • Make sure to eject to the Bare workflow if using react-native-webrtc!
  • If building via EAS Build, ensure your project is configured for native modules.

Permissions

Set up in app.json (or app.config.js):

Json
1{ 2 "expo": { 3 "android": { 4 "permissions": ["RECORD_AUDIO"] 5 }, 6 "ios": { 7 "infoPlist": { 8 "NSMicrophoneUsageDescription": "Microphone needed for voice recording." 9 } 10 } 11 } 12} 13

Handle runtime permissions in your code using Expo’s Permissions API.

Step 2: Creating the Offline Recorder

Requesting a MediaStream with Echo Cancellation

Js
1import { mediaDevices } from 'react-native-webrtc'; 2 3async function prepareRecording() { 4 const stream = await mediaDevices.getUserMedia({ 5 audio: { 6 echoCancellation: true, 7 noiseSuppression: true, 8 autoGainControl: true 9 } 10 }); 11 return stream; 12} 13

Recording to a File

WebRTC provides a raw audio stream (MediaStreamTrack). To store audio:

  • Direct WebRTC does not have a “MediaRecorder” in React Native; you must use a bridge library such as react-native-audio-record or process PCM data with react-native-fs.
  • Community tools like react-native-audio-record facilitate capturing PCM/WAV locally in sync with a media stream.

Hybrid Example (simplified):

Js
1import AudioRecord from 'react-native-audio-record'; 2 3AudioRecord.init({ 4 sampleRate: 44100, 5 channels: 1, 6 bitsPerSample: 16, 7 wavFile: 'recording.wav' 8}); 9 10AudioRecord.start(); 11// ...after some time 12AudioRecord.stop().then((audioFile) => { 13 // `audioFile` contains the path to the local recorded file 14}); 15

Step 3: Playback Locally

Example with expo-av:

Js
1import { Audio } from 'expo-av'; 2 3async function playRecording(uri) { 4 const { sound } = await Audio.Sound.createAsync({ uri }); 5 await sound.playAsync(); 6} 7

Step 4: Full Component Example

Js
1import React, { useRef, useState } from 'react'; 2import { View, Button, Text } from 'react-native'; 3import AudioRecord from 'react-native-audio-record'; 4import * as FileSystem from 'expo-file-system'; 5import { Audio } from 'expo-av'; 6 7export default function Recorder() { 8 const [recordingPath, setRecordingPath] = useState(null); 9 10 const startRecord = async () => { 11 AudioRecord.init({ 12 sampleRate: 44100, 13 channels: 1, 14 bitsPerSample: 16, 15 wavFile: 'recording.wav' 16 }); 17 AudioRecord.start(); 18 }; 19 20 const stopRecord = async () => { 21 const audioFile = await AudioRecord.stop(); 22 setRecordingPath(audioFile); 23 }; 24 25 const playBack = async () => { 26 if (recordingPath) { 27 await playRecording(recordingPath); 28 } 29 }; 30 31 return ( 32 <View> 33 <Button title="Start Recording" onPress={startRecord} /> 34 <Button title="Stop Recording" onPress={stopRecord} /> 35 <Button title="Play Recording" onPress={playBack} /> 36 {recordingPath && <Text>Recorded file: {recordingPath}</Text>} 37 </View> 38 ); 39} 40

This approach ensures recordings are local (offline) and, with proper capture constraints, echo cancellation is active.


5. Troubleshooting and Best Practices

Common Pitfalls

  • Permissions not granted: Always handle permission errors gracefully.
  • Echo cancellation not effective: Remind users to use phone speakers, not Bluetooth; echo suppression depends on the hardware.
  • Expo Managed workflow limitation: Managed workflow cannot link native modules required by react-native-webrtc. Always use EAS Build or eject to Bare.
  • Platform bugs: Test on both Android and iOS with real devices, as emulator audio stacks may not reflect reality.

Tips from the Community

  • For maximum EC, set all relevant constraints (echoCancellation, noiseSuppression, autoGainControl).
  • If advanced local streaming is critical (e.g., visualization), buffer and process PCM data with a worker thread.
  • Store files to persistent storage using FileSystem APIs for robust offline replay.
  • Test at different volumes and environments to verify echo is adequately suppressed.

Further Resources

Recommended Articles

Discover more articles you might find interesting

Implementing LangGraph REST API with FastAPI
Technical Insights

Implementing LangGraph REST API with FastAPI

This guide provides a comprehensive implementation plan for building a LangGraph REST API using FastAPI, covering environment setup, agent definitions, endpoint creation, testing, and deployment.

2101050
Jun 18
153
Read More
DeepSite v2 Practical Guide
Technical Insights

DeepSite v2 Practical Guide

A comprehensive guide to DeepSite v2, covering its features, installation, and advanced workflows.

2101050
Jun 21
112
Read More
Fastify OpenTelemetry: Logging, Metrics, and Tracing in Practice
Technical Insights

Fastify OpenTelemetry: Logging, Metrics, and Tracing in Practice

Learn how to implement logging, metrics, and tracing in Fastify using OpenTelemetry.

2101050
Jul 11
106
Read More
Creating Diverse Logo Designs with Flux Model and ComfyUI
Technical Insights

Creating Diverse Logo Designs with Flux Model and ComfyUI

Learn to leverage the Flux model and ComfyUI for unique logo designs through effective prompts and examples.

2101050
Jan 10
93
Read More
Formatting Dates in TypeScript to UTC
Technical Insights

Formatting Dates in TypeScript to UTC

A guide on how to format dates in TypeScript to the specific format YYYY-MM-DDTHH:mm:ss+00:00.

2101050
Dec 19
83
Read More
Implementing a Custom Chat Model with LangChain
Technical Insights

Implementing a Custom Chat Model with LangChain

This guide provides a comprehensive blueprint for creating a custom chat model by subclassing LangChain's BaseChatModel, including configuration, method overrides, and error handling.

2101050
Jun 17
78
Read More