Playing PCM Audio Streams with expo-dev-client in React Native

This tutorial covers the setup and implementation of PCM audio streaming in React Native using expo-dev-client and react-native-pcm-audio.

Blog cover image
2101050's avatar
2101050
26 views

Playing PCM Audio Streams with expo-dev-client in React Native

Introduction & Background

Pulse-code modulation (PCM) is the raw digital representation of audio, crucial for features such as voice chat, low-latency alerts, radio apps, or real-time audio feedback in mobile apps. In React Native, standard packages (e.g. expo-av) don’t support “raw” PCM streaming. Managed Expo Go apps cannot use arbitrary native modules, but expo-dev-client removes this limitation, letting you add native code and bridge native audio libraries—including those that support PCM buffers.

For practical use cases like:

  • Live voice chat streaming
  • Receiving and playing server-pushed PCM alerts
  • Audio processing or instrumentation apps

…it is essential to handle low-level PCM playback directly. This tutorial will walk you through setting up, coding, and troubleshooting PCM streaming using expo-dev-client and the react-native-pcm-audio package. You’ll find actionable instructions, code samples, and best practices, drawn from open-source documentation and real case studies.


Preparing Your React Native Project for PCM Audio Streaming

1. Setting Up the Project

Start by creating a new Expo project and setting it up for native development, which is required for PCM playback using native modules.

Bash
1npx create-expo-app my-pcm-audio-app 2cd my-pcm-audio-app 3npx expo install expo-dev-client 4npx expo prebuild # Converts to bare workflow, adds ios and android folders 5

Note: expo prebuild is necessary if your project is in the managed workflow. It adds the /ios and /android folders you need for native code.

2. Installing Native Dependencies

Next, add the PCM audio module:

Bash
1npm install react-native-pcm-audio 2# or 3yarn add react-native-pcm-audio 4

Link and Configure

  • On iOS: run npx pod-install
  • On Android: open Android Studio and ensure the module appears under native dependencies

3. Building Your Custom Dev Client

expo-dev-client allows you to run your app with any native code. You must always launch your app using your custom client, not Expo Go:

Bash
1npx expo run:android # On Android 2npx expo run:ios # On iOS (requires macOS/Xcode) 3

4. Troubleshooting Native Integration

If you encounter errors:

  • Double-check you’re running your built dev client, not Expo Go
  • Remove node_modules, clean CocoaPods/Gradle:
    Bash
    1rm -rf node_modules ios/Pods 2npm install 3npx pod-install 4
    For Android, run ./gradlew clean inside the android folder

For more troubleshooting steps, refer to:


Integrating and Using react-native-pcm-audio: Initialization, Playback, and Stream Management

PCM Session Options

PCM playback sessions have plenty of configurable parameters. You will primarily set:

  • encoding: e.g., '16bit', '8bit', 'float' (see docs)
  • usage: e.g., 'alarm', 'music', 'voicecall' (used by underlying OS audio APIs)
  • sampleRate: e.g., 16000 for 16kHz
  • channels: number of channels, 1 (mono) or 2 (stereo)
  • mode: 'streaming' (real-time) or 'static' (buffered, play once)

Example: Initializing the Audio Session

Javascript
1import { NativeModules } from 'react-native'; 2 3const sessionOptions = { 4 encoding: '16bit', 5 usage: 'alarm', 6 sampleRate: 16000, 7 channels: 1, 8 mode: 'streaming', 9}; 10 11function startPcmSession(options) { 12 return new Promise(resolve => { 13 NativeModules.PcmAudio.build(options, (event, data) => { 14 if (event === 'onSessionId') resolve(data); 15 }); 16 }); 17} 18

Writing PCM Buffers

Audio data should be written in base64-encoded binary form. Here’s how to convert and send audio frames (assuming a Int16Array buffer):

Javascript
1import base64 from 'react-native-base64'; 2 3function writePcm(sessionId, int16Array) { 4 const uint8Array = new Uint8Array(int16Array.buffer); 5 const base64Data = base64.encode(String.fromCharCode.apply(null, uint8Array)); 6 NativeModules.PcmAudio.write(sessionId, base64Data); 7} 8

Playing and Stopping Audio

Javascript
1NativeModules.PcmAudio.play(sessionId); // Start playback 2NativeModules.PcmAudio.stop(sessionId); // Stop playback 3NativeModules.PcmAudio.release(sessionId); // Release audio session 4

Example: Full Streaming Session

Javascript
1let pcmSessionId = null; 2 3async function setupAndPlayPcmStream(getNextPcmBuffer) { 4 pcmSessionId = await startPcmSession(sessionOptions); 5 6 // Playback loop 7 while (true) { 8 const buffer = await getNextPcmBuffer(); // Await PCM frames from stream/network/device 9 if (!buffer) break; 10 writePcm(pcmSessionId, buffer); 11 } 12} 13

Key Notes:

  • Always match PCM format to session options
  • Call release(sessionId) when done to free resources

Advanced PCM Streaming Scenarios and Real-World Examples

Case Study: Live Voice Streaming via WebSocket

Suppose your server sends Int16 PCM buffers via WebSocket (e.g., for a push-to-talk app). Here’s how you’d wire that up:

Javascript
1import React, { useRef, useEffect } from 'react'; 2 3function VoiceStreamPlayer({ wsUrl }) { 4 const sessionIdRef = useRef(null); 5 useEffect(() => { 6 let ws = new WebSocket(wsUrl); 7 8 startPcmSession(sessionOptions).then(sessionId => { 9 sessionIdRef.current = sessionId; 10 ws.onmessage = ({ data }) => { 11 let pcmData = new Int16Array(data); // adjust as needed 12 writePcm(sessionId, pcmData); 13 }; 14 }); 15 16 return () => { 17 ws.close(); 18 if (sessionIdRef.current) NativeModules.PcmAudio.release(sessionIdRef.current); 19 }; 20 }, [wsUrl]); 21 22 return null; 23} 24

Tips:

  • Ensure network buffers and audio playback are synchronized (avoid underruns)
  • Consider queueing or pacing if frames arrive too quickly

Playback Mode Comparison

  • Streaming Mode: Best for ongoing or real-time audio (voice chat, radio)
  • Static Mode: Best for playing preloaded PCM clips (alerts, notifications)
Javascript
1const staticOptions = { ...sessionOptions, mode: 'static' }; 2 3// Writing all buffers first 4startPcmSession(staticOptions).then(sessionId => { 5 buffers.forEach(buffer => writePcm(sessionId, buffer)); 6 NativeModules.PcmAudio.play(sessionId); 7}); 8

Buffer Conversion for Different Formats

Sometimes PCM arrives as an array buffer; convert as needed:

Javascript
1function arrayBufferToBase64(arrayBuffer) { 2 let binary = ''; 3 let bytes = new Uint8Array(arrayBuffer); 4 bytes.forEach(b => (binary += String.fromCharCode(b))); 5 return base64.encode(binary); 6} 7

Real Use: Emergency Broadcast Player

Multiple government apps use server-pushed PCM clips for alerts. A similar flow would:

  • Open a session on notification
  • Buffer and play PCM frames as they arrive until the alert ends
  • Release the session to conserve device battery/memory

Conclusion and Additional Resources

Summary Checklist

  • Use expo-dev-client for custom native modules
  • Install and link react-native-pcm-audio
  • Match PCM buffer format to session configuration (encoding, bit depth, sample rate, channels)
  • Use streaming for continuous audio, static for alerts/clips
  • Always release sessions on unmount/finish
  • Troubleshoot using project logs and double-check custom client builds

Further Reading & References

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