Skip to content

React Native Expo expo-av Audio: Record and play sound | ShillehTek

October 22, 2023

Video Tutorial (Optional)

Watch first if you want to see audio recording and playback working end to end in a React Native Expo app.

Project Overview

React Native Expo + expo-av Audio: In this tutorial, you build a React Native Expo app that uses the expo-av Audio module (plus Expo FileSystem) to record audio, save it, and play it back from a single button.

This pattern is useful for language learning apps, voice notes, podcast prototypes, and any app that needs basic voice capture.

  • Time: 15 to 30 minutes
  • Skill level: Beginner
  • What you will build: A simple Expo app that requests mic permission, records audio, saves it locally, and plays it back

Parts List

From ShillehTek

  • No ShillehTek-specific parts required for this software build.

External

  • Node.js and npm - required to run Expo tooling and install dependencies
  • Expo CLI - used to initialize an Expo project
  • React Native Expo app project - the app you will create
  • expo-av - provides recording and playback APIs
  • expo-file-system - used to create a folder and move the recorded file
  • @expo/vector-icons - used for the record/stop icon

Note: The Expo audio library can be buggy in the simulator. You may need to close and reopen the simulator, and make sure the simulator volume is turned up.

Step-by-Step Guide

Step 1 - Initialize an Expo app

Goal: Create a new Expo project you can run on a device or simulator.

What to do: Install Node.js and npm if you do not already have them: https://nodejs.org/en/download/.

Then install Expo CLI globally and initialize a new project.

Code:

npm install -g expo-cli
expo init my-new-app

Replace my-new-app with your preferred app name, then pick a template (blank is fine).

Expected result: A new Expo project folder is created and dependencies are installed.

Step 2 - Add the audio recording and playback code to your component

Goal: Request microphone permission, record audio, save it to the app filesystem, then play it back.

What to do: Add the following code to your component (for example, your App.js). This example uses a single button to toggle record and stop.

Code:

import { Text, TouchableOpacity, View, StyleSheet } from 'react-native';
import React, { useState, useEffect } from 'react';
import { Audio } from 'expo-av';
import * as FileSystem from 'expo-file-system';
import { FontAwesome } from '@expo/vector-icons';

export default function App() {
  
  const [recording, setRecording] = useState(null);
  const [recordingStatus, setRecordingStatus] = useState('idle');
  const [audioPermission, setAudioPermission] = useState(null);

  useEffect(() => {

    // Simply get recording permission upon first render
    async function getPermission() {
      await Audio.requestPermissionsAsync().then((permission) => {
        console.log('Permission Granted: ' + permission.granted);
        setAudioPermission(permission.granted)
      }).catch(error => {
        console.log(error);
      });
    }

    // Call function to get permission
    getPermission()
    // Cleanup upon first render
    return () => {
      if (recording) {
        stopRecording();
      }
    };
  }, []);

  async function startRecording() {
    try {
      // needed for IoS
      if (audioPermission) {
        await Audio.setAudioModeAsync({
          allowsRecordingIOS: true,
          playsInSilentModeIOS: true
        })
      }

      const newRecording = new Audio.Recording();
      console.log('Starting Recording')
      await newRecording.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY);
      await newRecording.startAsync();
      setRecording(newRecording);
      setRecordingStatus('recording');

    } catch (error) {
      console.error('Failed to start recording', error);
    }
  }

  async function stopRecording() {
    try {

      if (recordingStatus === 'recording') {
        console.log('Stopping Recording')
        await recording.stopAndUnloadAsync();
        const recordingUri = recording.getURI();

        // Create a file name for the recording
        const fileName = `recording-${Date.now()}.caf`;

        // Move the recording to the new directory with the new file name
        await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory + 'recordings/', { intermediates: true });
        await FileSystem.moveAsync({
          from: recordingUri,
          to: FileSystem.documentDirectory + 'recordings/' + `${fileName}`
        });

        // This is for simply playing the sound back
        const playbackObject = new Audio.Sound();
        await playbackObject.loadAsync({ uri: FileSystem.documentDirectory + 'recordings/' + `${fileName}` });
        await playbackObject.playAsync();

        // resert our states to record again
        setRecording(null);
        setRecordingStatus('stopped');
      }

    } catch (error) {
      console.error('Failed to stop recording', error);
    }
  }

  async function handleRecordButtonPress() {
    if (recording) {
      const audioUri = await stopRecording(recording);
      if (audioUri) {
        console.log('Saved audio file to', savedUri);
      }
    } else {
      await startRecording();
    }
  }

  return (
    <View style={styles.container}>
      <TouchableOpacity style={styles.button} onPress={handleRecordButtonPress}>
        <FontAwesome name={recording ? 'stop-circle' : 'circle'} size={64} color="white" />
      </TouchableOpacity>
      <Text style={styles.recordingStatusText}>{`Recording status: ${recordingStatus}`}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    alignItems: 'center',
    justifyContent: 'center',
    width: 128,
    height: 128,
    borderRadius: 64,
    backgroundColor: 'red',
  },
  recordingStatusText: {
    marginTop: 16,
  },
});

Expected result: Pressing the button starts recording, pressing again stops the recording, saves it to a recordings folder, and plays it back.

Step 3 - Understand what the main functions do

Goal: Know where to customize behavior (permissions, start, stop, and playback).

What to do: Review the responsibilities of each section:

  • useEffect() requests recording permission on first render and cleans up an active recording during unmount.
  • startRecording() sets the iOS audio mode, prepares a new recording, and starts it.
  • stopRecording() stops the recording, generates a filename, creates a recordings/ directory, moves the file there, then loads and plays it back.
  • handleRecordButtonPress() toggles between start and stop based on whether a recording exists.

The rest of the file is UI layout and styling, which you can keep as-is or customize.

Expected result: You can identify where permission, recording, file saving, and playback happen in the code.

Conclusion

You built a React Native Expo app using the expo-av Audio module that records audio, saves it to the device filesystem, and plays it back on demand. This gives you a solid starting point for voice notes, audio messaging, and podcast style features.

Want parts and tools for your next build? Shop at ShillehTek.com. If you want help tailoring a mobile plus IoT experience or building a custom solution for your product, check out our consulting services.