Front-End/React Native

[React Native] 리액트 네이티브 Expo Audio 녹음, 재생 구현하기

현기 2022. 11. 24. 17:15

Expo에서 오디오 녹음 및 재생을 구현할 수 있는

Audio라는 Expo SDK를 지원합니다.

 

저는 프로젝트에 음성 일기를 구현하기 위해

사용했습니다.

 

공식 문서에 예제 코드가 잘 나와있지만,

저는 이런저런 에러로 꽤나 고생했습니다. 😂

 


📝Audio

 

https://docs.expo.dev/versions/v47.0.0/sdk/audio/

 

Audio - Expo Documentation

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

docs.expo.dev

✔ Usage에 예제 코드가 잘 나와있습니다. 

    Open In Snack 눌러서 직접 확인해 보세요!

 

⦁ 음성 녹음 예제 코드

import * as React from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
import { Audio } from 'expo-av';

export default function App() {
  const [recording, setRecording] = React.useState();

  //녹음 시작
  async function startRecording() {
    try {
      console.log('Requesting permissions..');
      await Audio.requestPermissionsAsync();
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: true,
        playsInSilentModeIOS: true,
      });

      console.log('Starting recording..');
      const { recording } = await Audio.Recording.createAsync( Audio.RecordingOptionsPresets.HIGH_QUALITY
      );
      setRecording(recording);
      console.log('Recording started');
    } catch (err) {
      console.error('Failed to start recording', err);
    }
  }

  //녹음 종료
  async function stopRecording() {
    console.log('Stopping recording..');
    setRecording(undefined);
    await recording.stopAndUnloadAsync();
    await Audio.setAudioModeAsync({
      allowsRecordingIOS: false,
    });
    const uri = recording.getURI();
    console.log('Recording stopped and stored at', uri);
  }

  return (
    <View style={styles.container}>
      <Button
        title={recording ? 'Stop Recording' : 'Start Recording'}
        onPress={recording ? stopRecording : startRecording}
      />
    </View>
  );
}

const styles = StyleSheet.create({ ... });

 

📑 startRecording() 메서드를 통해 음성 녹음을 시작합니다.

 

1. await Audio.requestPermissionsAsync( );  

✔ 녹음 권한을 요청합니다.

 

 

2. await Audio.setAudioModeAsync( );
✔ playsInSilentModeIOS 등의 녹음 설정이 가능합니다.

 


3. (중요) const { recording, status } = await Audio.Recording.createAsync( )

✔ 가장 핵심이 되는 코드입니다. 녹음을 만들고 시작합니다.
recording는 녹음한 오디오 객체, status는 오디오 파일의 정보를 담고 있습니다.

 

✔ 메서드 하나로 다음 코드와 동일한 결과를 얻을 수 있습니다.
    하지만 아래와 같이 사용하면 HIGH_QUALITY 말고도, 더 세부적인 options을 설정할 수 있습니다.
    예를 들면, 구글 Speech To Text API가 더 잘 인식하는 옵션이 있다고 하네요.
    하단 참고 문헌에 링크 달아놓겠습니다. 👍

const recording = new Audio.Recording();
await recording.prepareToRecordAsync(options); recording.setOnRecordingStatusUpdate(onRecordingStatusUpdate);
await recording.startAsync();

 

 

📑 stopRecording() 음성 녹음 종료 메서드

...

  async function stopRecording() {
    console.log('Stopping recording..');
    setRecording(undefined);
    await recording.stopAndUnloadAsync();
    await Audio.setAudioModeAsync({
      allowsRecordingIOS: false,
    });
    const uri = recording.getURI();
    console.log('Recording stopped and stored at', uri);
  }
  
  ...

 

1. await recording.stopAndUnloadAsync( );  

✔ 녹음을 중지하고 메모리에서 레코더 할당을 해제합니다.

 

2. (중요) const uri = recording.getURI( );

✔ 저장된 녹음 파일의 위치를 가져옵니다.

// getURI() 결과
file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540qksw5592%252Fsodam/Audio/recording-55edf3b7-f139-4e23-aa33-c0542f272fb3.m4a

 

📑 활용 예시

// uri를 통해 재생을 하고싶다면 사용
const playAudio = async () => {
    const sound = new Audio.Sound();
    await sound.loadAsync({ uri: 녹음파일URI });
    console.log('Playing Sound');
    await sound.replayAsync();
}

 sound 객체를 생성하고 loadAsync( {uri : 녹음 파일 위치} )를 통해

    녹음을 재생시킬 수 있습니다.

 

    이 녹음 파일의 정보를 어디에 저장시킬 지가 문제인데,

    조사한 바에 따르면 Expo File System, 구글 클라우드, AWS S3 등에

    저장해서 활용한다고 하네요.

    저는 AsyncStorage를 활용해 다른 컴포넌트에서 재생 가능한지

    확인해 봤더니 잘 동작했습니다. 👍

 

최종적으로 어떤 저장소를 사용하게 될지는 더 고민해 봐야겠습니다.. 🤦‍♂️

 

📝 AsyncStorage 활용 예시

더보기

⦁ AudioRecorder.js

import React, {useEffect} from 'react';
import { Text, View, StyleSheet, Button, Touchable, TouchableOpacity } from 'react-native';
import { Audio } from 'expo-av';
import { SimpleLineIcons } from '@expo/vector-icons';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';

//녹음 설정
const recordingOptions = {
    android: {
        extension: '.m4a',
        outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
        audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
        sampleRate: 44100,
        numberOfChannels: 2,
        bitRate: 128000,
    },
    ios: {
        extension: '.wav',
        audioQuality: Audio.RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH,
        sampleRate: 44100,
        numberOfChannels: 1,
        bitRate: 128000,
        linearPCMBitDepth: 16,
        linearPCMIsBigEndian: false,
        linearPCMIsFloat: false,
    },
};

export default function AudioRecorder() {
  const [recording, setRecording] = React.useState();

  //녹음 시작
  async function startRecording() {
    try {
      console.log('Requesting permissions..');
      await Audio.requestPermissionsAsync();
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: true,
        playsInSilentModeIOS: true,
      });

      console.log('Starting recording..');
      const recording = new Audio.Recording();
      await recording.prepareToRecordAsync(recordingOptions);
      await recording.startAsync();

      setRecording(recording);
      console.log('Recording started');

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

  //녹음 종료
  async function stopRecording() {
    setRecording(undefined);
    await recording.stopAndUnloadAsync();
  
    //녹음파일 객체 생성
    const {sound, status} = await recording.createNewLoadedSoundAsync();
    
    const audio = {
      sound : sound,
      file : recording.getURI(),
      duration: getDurationFormatted(status.durationMillis),
      status : status
    }

    //스토리지 저장
    await AsyncStorage.setItem("audio", JSON.stringify(audio))
  }

  //시간 표시
  function getDurationFormatted(millis) {
    const minutes = millis / 1000 / 60;
    const minutesDisplay = Math.floor(minutes);
    const seconds = Math.round((minutes - minutesDisplay) * 60);
    const secondsDisplay = seconds < 10 ? `0${seconds}` : seconds;
    return `${minutesDisplay}:${secondsDisplay}`;
  }

  return (
    <View style={styles.container}>
      <TouchableOpacity style={{justifyContent:'center', alignItems:'center'}} onPress={recording ? stopRecording : startRecording}>
        {recording ?
          <>
          <MaterialCommunityIcons name="stop" size={24} color="black" />
          <Text style={styles.noteText}>녹음종료</Text>
          </>
          :
          <>
          <SimpleLineIcons name="microphone" size={24} color="black" />
          <Text style={styles.noteText}>음성일기</Text>          
          </>
        }
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    width:44,
    height:44,
  },
  noteText:{
    fontSize:12,
    marginTop:5,
  },
  button: {
    width:50,
    margin: 16
  }
});

 

⦁ AudioPlayer.js

import * as React from 'react';
import { Button, View, Text, StyleSheet } from 'react-native';
import { Audio } from 'expo-av';
import AsyncStorage from '@react-native-async-storage/async-storage';

export default function AudioPlayer() {
    const [audioData, setAudioData] = React.useState({});

    React.useEffect(() => {
        getData()
    }, [])

    //스토리지 audio 객체 가져오기
    const getData = async () => {
        try {
            const audio = await AsyncStorage.getItem("audio")
            if (audio) {
                setAudioData(JSON.parse(audio))
            }
        } catch (e) {
            console.log(e)
        }
    }

    const playAudio = async () => {
        const sound = new Audio.Sound();
        await sound.loadAsync({ uri: audioData.file });
        console.log('Playing Sound');
        await sound.replayAsync();
    }

    return (
        <View style={{ width: 200, height: 200 }}>
            {audioData.sound &&
                <>
                    <Text>{audioData.duration}</Text>
                    <Button style={styles.button} onPress={playAudio} title="Play"></Button>
                </>
            }
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        width: 44,
        height: 44,
    },
    noteText: {
        fontSize: 12,
        marginTop: 5,
    },
    button: {
        width: 50,
        margin: 16
    }
});

 



 


📝 Youtube 강의

 

 

https://www.youtube.com/watch?v=pd_Ez9Kbi2c 

 

✔ 공부하면서 관련 자료를 찾다가 해외 유튜버 분의 Expo Audio 튜토리얼 영상을 발견했습니다.

    저에게는 많은 도움이 되었기 때문에 추천드립니다.  

 

⦁ Github 링크

https://github.com/chelseafarley/expo-av-demo/blob/master/App.js

 

GitHub - chelseafarley/expo-av-demo: Demo of expo av

Demo of expo av. Contribute to chelseafarley/expo-av-demo development by creating an account on GitHub.

github.com

 


 


드디어 모바일로 해보고 싶었던 기능 중

하나인 Audio를 공부해 봤습니다.

 

공식 문서 보는 것도 헷갈리고 힘들었네요

특히 undefined가 정말 미웠습니다. 😂

 

이제 UI 디자인 입히고,

슬라이더, 일시정지 등 구현해서 프로젝트에

합쳐야겠습니다.

 

제가 구글에서 헤엄치면서 찾은

보석 같은 사이트들을 참고 문헌에 링크해뒀으니,

관심 있으신 분들은 살펴보시면 좋을 것 같습니다. 🔥

 


참고 문헌 : 

 

(강추)

https://fostermade.co/blog/making-speech-to-text-work-with-react-native-and-expo

 

https://blog.naver.com/PostView.naver?blogId=mogulist&logNo=221621055553&parentCategoryNo=&categoryNo=39&viewDate=&isShowPopularPosts=true&from=search         

 

https://www.youtube.com/watch?v=pd_Ez9Kbi2c