[iOS] AVFoundation을 이용하여 녹음과 재생하기

Picnic·2025년 1월 9일

iOS

목록 보기
4/5
post-thumbnail

(처음으로 썸네일을 만들면서 애플 문서에 있는 스타일대로 만들어봤는데 괜찮은 거 같기도🫠)

안녕하세요 Picnic🧃입니다.
오늘은 AVFoundation을 이용하여 아이폰에 녹음을 하고 재생을 하는 방법을 알아볼 거에요.
먼저 녹음을 위해서는 실기기가 필요합니다. 이전에는 SwiftUI의 프리뷰나 시뮬레이터에서 녹음이 되는 걸로 알고 있었는데 제 컴퓨터 환경에서는 되지가 않네요. 혹시 해결 방법을 알고 계신 분들은 댓글 남겨주시면 감사하겠습니다!

Xcode: 16.2
macOS Sequoia 15.1.1
Development Target: iOS 16.0


그러면 시작!

뭐가 필요한가?

먼저 녹음과 재생을 하기 위해서는 무엇이 필요한지 알아야겠죠!
바로 아래와 같은 것들이 필요합니다!

  • AVAudioSession
  • AVAudioRecorder
  • AVAudioPlayer

원래는 더 많은 클래스들이 있지만 여기서는 기본적인 방법에 대해 알아볼 것이기 때문에 위의 세 가지 정도만 있어도 됩니다!


그러면 하나하나 살펴보도록 하죠!

AVAudioSession

AVAudioSession의 정의는 앱에서 오디오를 어떻게 사용하려는지 시스템에 전달하는 객체입니다.

오디오 세션은 앱과 운영체제 사이의 중개자 역할을 하며, 결과적으로 기본 오디오 하드웨어 역할을 합니다. 오디오 세션을 사용하여 특정 동작이나 오디오 하드웨어와의 필요한 상호 작용을 자세히 설명하지 않고 앱 오디오의 일반적인 특성을 운영 체제에 전달합니다.

따라서 세부 사항의 관리를 오디오 세션에 위임하면 운영체제가 가장 잘 관리를 해준다고 하네요!

모든 iOS, tvOS 및 watchOS 앱에는 다음과 같은 동작으로 사전 구성된 기본 오디오 세션이 있습니다.

  • 오디오 재생을 지원하지만, 오디오 녹음은 허용하지 않습니다.
  • 앱이 오디오를 재생하면 다른 백그라운드 오디오를 음소거합니다.
  • iOS에서 무음 스위치를 무음 모드로 설정하면 앱이 재생하는 모든 오디오가 무음됩니다.
  • iOS에서 장치를 잠그면 앱의 오디오가 무음이 됩니다.

기본 오디오 세션은 유용한 동작을 제공하지만, 일반적으로 앱에 필요한 오디오 동작을 제공하지는 않는다고 합니다.
따라서 기본 동작을 변경하려면 앱의 오디오 세션 카테고리를 구성해야 합니다.

이 카테고리에는 여러가지가 있지만 나중에 살펴보도록 할게요.

AVAudioSession은 오디오 세션의 카테고리, 모드, 및 기타 구성을 설정하는데 사용되는 싱글톤 객체입니다.

그렇다면 세션을 어떻게 구성하는지 알아볼까요?

세션 구성하기

오디오 세션은 다음과 같이 구성할 수 있습니다.

func setupAudioSession() {
    do {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
        try session.setActive(true)
    } catch {
        fatalError("Failed to configure and activate session.")
    }
}
  1. 먼저 오디오 세션은 싱글톤 객체라고 했으니까 싱글톤으로 객체를 가져와 줍니다.
  2. 그리고 setCategory(_:mode:options:) 메서드를 통해 세션 카테고리를 구성해 줍니다.
  3. setActive(_:) 메서드를 통해 세션을 활성화시켜 줍니다.

카테고리는 아래에 작성해 놓았습니다.


생각보다 간단하죠?🫠
간단히 설명하자면 오디오 세션 객체를 가져와서 녹음과 재생을 할 수 있는 카테고리를 설정해주고 활성화 해준 것입니다.
이 때 카테고리를 설정해놓으면 활성화 하는것은 원할 때 하면 되지만 앱이 오디오 재생을 시작할 때 호출하는 것이 바람직하다고 해요.



그럼 세션을 구성한 뒤에는 뭘 해야 할까요?


AVAudioRecorder

세션을 .playAndRecord로 설정을 했기 때문에 녹음와 재생을 할 수 있는 기본적인 설정은 끝났습니다.
그렇다면 녹음을 어떻게 해야 하는지부터 살펴볼게요.

AVAudioRecorder오디오 데이터를 파일에 기록하는 객체입니다.

오디오 레코더를 사용하면

  • 시스템의 활성 입력 장치에서 오디오를 녹음합니다.
  • 지정된 기간 동안 또는 사용자가 녹음을 중단할 수 있습니다.
  • 녹음을 일시 중지하고 재개할 수 있습니다.
  • recording-level의 계량 데이터에 엑세스할 수 있습니다.

iOS 또는 tvOS에서 오디오를 녹음하려면, record 또는 playAndRecord 카테고리를 사용하도록 오디오 세션을 구성해야 합니다.

여기서 recording-level은 녹음할 때 얼마나 큰 소리로 녹음할 것인지를 나타내는 데이터입니다.

그러면 이를 이용해 어떻게 녹음을 하는지 알아볼게요.

실기기로 테스트시 권한 설정을 반드시 해주세요!
그렇지 않으면 앱이 강제종료될 수 있습니다.


녹음 시작하기

var recorder: AVAudioRecorder!

func setupRecorder() {
    // 1. 녹음 파일을 저장할 디렉토리 설정
    let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
    let fileURL = tempDir.appendingPathComponent("recording.wav")
    
    do {
				// 2. 오디오 세팅값 설정
        let settings: [String: Any] = [
            AVFormatIDKey: Int(kAudioFormatLinearPCM),
            AVLinearPCMIsNonInterleaved: false,
            AVSampleRateKey: 44_100.0,
            AVNumberOfChannelsKey: 1,
            AVLinearPCMBitDepthKey: 16
        ]
        // 3. 레코더 생성
        recorder = try AVAudioRecorder(url: fileURL, settings: settings)
    } catch {
        fatalError("Unable to create audio recorder: \(error.localizedDescription)")
    }
    
    recorder.delegate = self
    recorder.prepareToRecord()
}

func record() -> Bool {
    setupRecorder()
    let started = recorder.record() // 녹음
    return started
}

오디오 세션을 설정하는 것보다는 코드가 길어졌죠..?
하지만 크게 본다면 그렇게 어렵지 않습니다!

자세한 내용을 배제하고 큰 순서만 본다면

  1. 녹음 파일을 저장할 디렉토리를 설정한다.
  2. 오디오 세팅 값을 설정한다.
  3. 디렉토리 경로와 오디오 세팅 값을 이용하여 레코더를 생성한다.
  4. record() 메서드를 이용해 녹음을 시작한다!

의 순서입니다. 생각보다 어렵지 않죠? 🎨
(사실 나에게는 어려웠기 때문에 그나마 쉬워 보이려고 글을 쓰는…)



아무튼!
그럼 한 번 자세히 살펴볼게요.

일단 구현을 먼저 하고 싶다! 하시는 분들은 [녹음 완료하기]로 바로 가시면 됩니다!


먼저 녹음 파일을 저장할 경로를 설정해주는 것은 원하는 디렉토리의 경로를 얻어서 원하는 제목으로 경로를 설정하면 됩니다.

위 코드에서는 Temp 폴더에 저장하는 것을 알 수 있어요. (이는 애플의 예제 코드를 예로 들면서 살펴보고 있기 때문에 Temp로 되어 있는데 본인의 선택에 따라 바로 Documents에 저장할 수 있습니다!)

그리고 오디오 세팅 값을 설정해줍니다… 여기서 의미를 모르겠는 값들이 마구 쏟아지는데…
👀 차근차근 살펴볼게요.

먼저 AVAudioRecorder를 생성하는데 필요한 메서드는 init(url:settings:) 입니다.
(이것만 있는 것은 아닙니다!)

이때 url은 설정했으니 settings에 들어갈 딕셔너리를 구성해야 하는데 이 딕셔너리는 특수한 타입이 있는 것은 아니고 [String : Any] 타입입니다.

시스템은 아래와 같은 키, 값을 지원하고 있습니다.

음… 일단 이게 있다는 것은 알겠네요..?
살펴볼…까요?

AVFormatIdKey

AVFormatIDKey: 오디오 데이터의 형식을 나타내는 정수 값입니다.

이에 해당하는 값들 여기에 굉장히 많이 나와 있는데요, 지원하는 값들이 나와 있으니 오른쪽의 값들을 볼게요.

  • kAudioFormatLinwaePCM: 선형 PCM 코덱을 지정하고 표준 플래그를 사용하는 키.
  • kAudioFormatMPEG4AAC: MPEG-4 AAC Low Complexity 코덱을 지정하고 플래그를 사용하지 않는 키.
  • kAudioFormatAppleLossless: Apple Lossless 코덱을 지정하고 플래그를 사용하여 소스 자료의 비트 깊이를 나타내는 키.
  • kAudioFormatAppleMA4: 애플의 IMA 4:1 ADPCM 코덱 구현을 지정하고 플래그를 사용하지 않는 키.
  • kAudioFormatiLBC: 인터넷 저 비트레이트 코덱(iLBC) 협대역 음성 코덱을 지정하고 플래그를 사용하지 않는 키.
  • kAudioFormatLaw: μ-Law 2:1 코덱을 지정하고 플래그를 사용하지 않는 키.



음… 코덱이 뭐죠? 😳

코덱(CODEC)이란 인코더와 디코더의 합성어로 인코딩과 디코딩 기능을 함께 갖춘 기술 또는 기술을 갖춘 하드웨어나 소프트웨어…!!! 😱

몰랐었는데 알고 나니 조금 가까워진 느낌이네요 하하…



설명을 보니 각 값들은 어떤 코덱을 정할지를 나타내는 것 같아요.
즉, 어떤 방식으로 인코딩과 디코딩을 할 것인지! 아하!

그런데 각 코덱을 알기에는 너무 어려운 것 같은데… 자주 사용되는 것들만 볼게요!

  • kAudioFormatMPEG4AAC: 효율적인 압축을 제공하며, 대부분의 기기와 호환
    파일 크기를 최적화하거나 고품질 녹음을 해야 하는 것이 아니라면 대부분 이 코덱을 사용하면 되겠네요!
  • kAudioFormatLinearPCM: 비압축 포맷으로 고품질이지만 파일 크기가 큼.
  • kAudioFormatAppleLossless: 품질을 유지하며 압축.



AVSampleRateKey

AVSampleRateKey: 샘플 레이트를 나타내는 부동 소수점 값, 헤르츠 단위입니다.

여기서 샘플 레이트란, 1초에 샘플링을 몇 번 할 것인지를 말합니다.
샘플링을 한다는 것은 아날로그 신호를 디지털 신호로 변환 시키는 것을 말하죠!


일반적으로는 다음과 같은 샘플 레이트가 사용된다고 합니다.

  • 일반 오디오: 44100(CD 품질)
  • 고품질 오디오: 48000
  • 음성 녹음: 8000(저품질), 16000(표준)



AVNumberOfChannelsKey

AVSampleRateKey: 채널 수를 나타내는 정수 값.

여기서 채널이란 하나의 음원 안에 독립적으로 저장/재생되는 소리 각각을 채널이라고 합니다.
채널 수가 1인 것을 모노(mono)라고 하는데 이는 양쪽 스피커에서 나오는 소리가 같습니다.
채널 수가 2인 경우 스테레오(stereo)라고 하는데 각 채널에 담긴 정보가 다르기 때문에 각각의 스피커에서 나오는 소리가 다릅니다.

코드를 통해 현재 기기가 스테레오 녹음을 지원하는지 확인할 수 있습니다.
이를 확인하기 위해서는 조금 더 알아야 할 개념들이 등장하기 때문에 지금은 모노로만 예시를 들게요!



더 있나?

일단 기본적인 세팅 값들에 대해서는 알아봤습니다.

하지만 이것만 있는 것은 아닌데요, 예를 들어 코덱을 kAudioFormatLinwaePCM로 설정했다면 그에 관한 추가 설정들을 더 할 수 있습니다. 또한, 인코더에 관해서도 추가적으로 설정할 수 있습니다.

이에 관한 링크는 [참고] 섹션에 적어둘게요. 🫠



녹음 완료하기

후… 쉽게 쉽게 갈 줄 알았는데 중간에 생각보다 긴 과정이 있었네요.
그럼 바로 녹음을 완료하는 방법을 알아볼게요.

녹음을 완료하는 방법은 다음과 같습니다.

func stopRecording() {
    recorder.stop()
}

…😮

끝입니다.

대신 녹음 파일을 디렉토리에 저장해줘야겠죠!

extension RecordViewModel: AVAudioRecorderDelegate {
    
    // The AVAudioRecorderDelegate method.
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        
        let destURL = FileManager.default.urlInDocumentsDirectory(named: "recording.wav")
        try? FileManager.default.removeItem(at: destURL)
        try? FileManager.default.copyItem(at: recorder.url, to: destURL)
        
        recorder.prepareToRecord()
        
        audioURL = destURL
    }
}

[녹음 시작하기]에서 마지막에 delegate를 self로 설정해 준 것을 보셨나요?
Delegate를 통해 녹음이 시작되거나 끝날 때를 이용할 수 있는 메서드들이 있습니다!

위 예시에서는 destURL로 저장 위치를 설정해 주고, 먼저 해당 위치에 같은 파일이 있으면 지워줍니다.
그리고 레코더로 기록한 파일 위치에서 파일을 destURL로 복사합니다.

애플의 예제 코드에서는 위와 같이 작성을 하였는데, 익숙하게 하려면 Documtents에 바로 저장하는 방법도 있겠죠!
만약 파일 시스템에 저장하지 않고 실행시키길 원한다면 recorder.url을 변수에 저장해놓으면 될 거에요.

그리고 [녹음 시작하기]에서와 이 예제에서 보이는 한 코드가 있습니다.
recorder.prepareToRecord() 인데요. 이 코드는 오디오 파일을 만들고 녹음을 위한 시스템을 준비합니다.
굉장히 중요한 코드 같은데요.

이 메서드를 호출하면 레코더를 만들 때 사용한 URL에 오디오 파일이 생성됩니다.
파일이 이미 해당 위치에 존재하는 경우, 덮어씁니다.

사실 레코더의 record() 메서드를 사용하면 암시적으로 prepareToRecord() 메서드를 호출합니다.
하지만 record()를 호출할 때 가능한 한 빨리 녹음을 시작할 수 있도록 prepareToRecord()를 호출할 수 있습니다!

예제 코드에서는 같은 이름으로 하나의 파일만을 저장하기 때문에 prepareToRecord()를 여기에 넣은 것 같은 느낌이네요.


AVAudioPlayer

길고 긴 녹음하기를 지나 이제 재생하기입니다!
(금방 끝날 줄 알았는데 생각보다 길어ㅈ…🫠)

녹음을 할 때는 AVAudioRecorder를 사용했다면 재생을 할 때에는 AVAudioPlayer를 사용합니다!
이름에서 쉽게 알 수 있죠!

AVAudioPlayer파일 또는 버퍼에서 오디오 데이터를 재생하는 객체입니다.

오디오 플레이어를 사용하여

  • 파일 또는 버퍼에서 모든 기간(duration)의 오디오를 재생합니다.
  • 재생된 오디오의 볼륨, 패닝, 속도 및 반복 동작을 제어합니다.
  • 재생 레벨 계량 데이터에 액세스합니다.
  • 여러 플레이어의 재생을 동기화하여 여러 소리를 동시에 재생합니다.

여기서 패닝은 사운드 소스의 위치를 가상의 공간에서 조절하는 오디오 프로덕션 기술입니다.
예를 들어 왼쪽 귀와 오른쪽 귀에서 들리는 소리를 조절하는 것이 있습니다.

재생을 위한 객체! 좋아요!

그럼 어떻게 재생하는지 알아볼까요?

재생 시작하기

아까 녹음을 시작하려면 레코더를 설정해 주었는데요, 재생에서도 마찬가지입니다.

var player: AVAudioPlayer?

func play() {
		// 1. 재생할 파일의 위치 확인
    guard let url = audioURL else { print("No recording to play"); return }

    do {
		    // 2. 오디오 플레이어 생성
        player = try AVAudioPlayer(contentsOf: url)
        // 3. 재생할 준비가 되었는지 확인
        if player!.prepareToPlay() {
		        // 4. 재생 준비가 되었다면 파일 재생
            player!.play()
            player!.delegate = self
        } else {
		        // 재생 실패
            print("player doesn't prepare yet.")
        }
    } catch {
		    // 플레이어 생성 실패
        print("AudioPlayerError: \(error.localizedDescription)")
        return
    }
}

위 코드가 조금 복잡해 보이지만 사실 아래와 같이 작성해도 무방합니다.

func play() {
		// 1. 재생할 파일의 위치 확인
    guard let url = audioURL else { print("No recording to play"); return }

    do {
		    // 2. 오디오 플레이어 생성
        player = try AVAudioPlayer(contentsOf: url)
		    // 3. 재생 준비가 되었다면 파일 재생
        player!.play()
        player!.delegate = self
    } catch {
		    // 플레이어 생성 실패
        print("AudioPlayerError: \(error.localizedDescription)")
        return
    }
}

이번에도 큰 순서를 볼게요.

  1. 재생할 파일의 위치를 확인한다.
  2. 오디오 플레이어를 생성한다.(AVAudioPlayer의 이니셜라이저 또한 여러 가지가 있습니다.)
  3. 재생한다.

간단하죠?

위 코드와 아래 코드의 차이점은 prepareToPlay() 메서드의 유무입니다.

prepareToPlay() 메서드는 오디오 재생을 위해 플레이어를 준비합니다.
이 메서드를 호출하면 오디오 버퍼가 미리 로드되고 재생에 필요한 오디오 하드웨어가 획득됩니다.

또한 이 메서드는 오디오 세션을 활성화하므로 즉시 재생이 필요하지 않은 경우 setActice(_:error:)에 false를 전달합니다.

prepareToPlay()prepareToRecord()와 달리 오디오 세션을 활성화시켜주네요?

prepareToPlay() 역시 prepareToRecord()가 그랬던 것처럼 play() 메서드 호출 시 자동으로 호출됩니다.
하지만 미리 호출하면 play() 호출과 사운드 출력 사이의 지연이 최소화된다고 합니다!

그리고 첫 번째 코드에서 prepareToPlay() 를 if 문에서 사용할 수 있는 이유는 이 메서드가 Bool을 리턴하기 때문입니다.
또한 play() 도 Bool을 호출합니다! 따라서 준비가 되었는지 확인하고 그에 맞게 재생을 시킬 수 있습니다.

그리고 이는 record()에도 동일하게 적용된다는 것!



재생 멈추기

재생을 멈추는 법은…

func stopPlayback() {
    player?.stop()
}

쉽죠..?
여기서 player가 옵셔널 체이닝이 있는 이유는 player를 옵셔널로 선언했기 때문이에요.
recorder는 !로 선언한 것과 달리 특별한 이유가 있어서 다르게 둔 건 아닙니다. 👀

녹음에서는 녹음을 멈추고 저장하는 일을 했는데 재생을 필요 없을까요?

네?

재생을 멈추고 나서의 필수적인 행동은 없습니다.
다만 player에도 delegate가 있듯이 재생을 멈춘 후에 호출되는 메서드가 있습니다.(물론 이것만 있는 것은 아님)

extension RecordViewModel: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        isPlaying = false
    }
}

다음과 같이 자유롭게 재생 종료 후 원하는 작업을 하시면 됩니다.

여기까지 해서 기본적인 오디오 녹음, 재생의 과정을 살펴봤습니다! 후…
쉽게 쓰고 싶었는데 잘 쓴 건지 모르겠네요. 나중에 까먹어서 다시 찾아봤을 때 이해가 잘 되기를.. 🙏


더 있나?

물론이겠죠! 이렇게 적게만 있을 리가!

위에서 사용된 클래스와 메서드들 말고도 굉장히 많은 것들이 있습니다.
가볍게만 쓰고 넘어갈게요!

AVAudioEngine

AVAudioEngine은 조금 더 고급 기능을 사용할 수 있는 객체입니다.

녹음의 경우 녹음된 오디오에 신호 처리를 적용하거나, 재생의 경우 스트리밍 또는 positional audio 재생과 같은 기능을 사용할 수 있습니다.

더 많은 메서드

녹음과 재생 부분에서 더 많은 메서들이 있습니다.

앞에서는 단순히 녹음과 재생만 다뤘지만, 특정 시간만큼만 녹음한다던가, 특정 지점부터 재생한다던가, 녹음, 재생 일시정지와 같은 메서드들도 있습니다. 이를 통해 더 다양한 경험을 제공할 수 있겠죠! (사실 일시정지 같은 건 기본으로 들어가야 하는…)



Session Category

앞에서 가볍게(?) 무시하고 넘어갔었죠?
종류가 엄청 많지는 않으니 한 번 알아볼게요.

오디오 세션 카테고리는 오디오 동작 세트를 정의합니다.
필요한 오디오 동작을 가장 정확하게 설명하는 카테고리를 선택하세요.

라고 합니다.

편의상 카테고리는 GPT에게 물어보겠ㅅ…

설명이 꽤 길어서 접어둘게요....였는데 접기가 없네😐

이 부분은 필요한게 아니면 빠르게 넘어가도 됩니다!

1. ambient

설명:

  • 앱의 사운드 재생이 주요 기능이 아닌 경우 사용하는 카테고리입니다.
    앱은 사운드 없이도 동작해야 하며, 사용자가 장치의 음량을 끄거나 다른 앱의 오디오를 우선시할 수 있습니다.

특징:

  • 다른 앱의 오디오 재생과 공존 가능.
  • 사용자가 장치의 음소거 스위치를 켜면 앱의 소리가 나지 않음.

사용 사례:

  • 백그라운드 사운드(예: 자연의 소리) 또는 효과음이 있는 앱.
  • 게임에서 배경 음악이 필수적이지 않은 경우.


2. multiRoute

설명:

  • 서로 다른 오디오 데이터를 동시에 여러 출력 장치로 라우팅할 수 있는 카테고리입니다.

특징:

  • 다중 출력 지원(예: 헤드폰과 스피커를 동시에 사용).
  • 고급 오디오 설정이 필요한 경우 적합.

사용 사례:

  • 오디오 믹싱 앱.
  • 디제이 및 음악 제작 애플리케이션.


3. playAndRecord

설명:

  • 오디오 녹음(입력)과 재생(출력)이 동시에 필요한 앱을 위한 카테고리입니다.

특징:

  • 마이크 입력과 스피커 출력을 모두 활성화.
  • VoIP와 같은 양방향 오디오 통신 지원.
  • 특정 장치(예: Bluetooth 헤드셋)에서 입출력 전환 지원.

사용 사례:

  • VoIP 앱(예: Zoom, Skype).
  • 오디오 녹음 및 피드백 제공 앱.
  • 음성 메모 앱.

앞의 예제에서 사용했었죠!

4. playback

설명:

  • 녹음된 음악이나 사운드 재생이 앱의 중심적인 기능일 때 사용하는 카테고리입니다.

특징:

  • 오디오 재생이 다른 앱의 오디오 세션을 중단시킴.
  • 장치의 음소거 스위치를 무시하고 항상 소리를 재생.
  • 백그라운드 재생 지원.

사용 사례:

  • 음악 스트리밍 앱(예: Spotify, Apple Music).
  • 비디오 플레이어 앱.
  • 오디오북 앱.


5. record

설명:

  • 오디오를 녹음하기 위한 카테고리입니다. 녹음 중에는 출력 오디오(재생)가 침묵됩니다.

특징:

  • 입력 장치(마이크)만 활성화.
  • 기존 재생 오디오 중단.

사용 사례:

  • 음성 녹음 앱(예: 음성 메모).
  • 오디오 캡처가 핵심 기능인 앱.


6. soloAmbient

설명:

  • 기본 오디오 세션 카테고리입니다. 앱의 사운드 재생이 비주요 기능일 때 사용되며, ambient와 비슷하지만 오디오 세션을 독점합니다.

특징:

  • 다른 앱의 오디오를 중단.
  • 음소거 스위치가 켜져 있을 때 소리가 나지 않음.
  • 배경 음악보다 효과음이 중요한 앱에 적합.

사용 사례:

  • 게임이나 유사한 앱에서 효과음 재생.
  • 단순 알림음이나 사운드 효과 재생이 필요한 앱.

참고로 백그라운드 재생같은 경우는 Capability 설정을 따로 해줘야 합니다!

mode와 options는 문서를 살펴보면 나름 이해하실 수 있을거에요.
(글 쓰다 지쳐서 넘기는거 아님(맞음))


코드

AVFoundation을 이용한 뷰모델의 코드는 다음과 같이 사용할 수 있습니다.

class RecordViewModel: NSObject, ObservableObject {
    
    private var player: AVAudioPlayer?
    private var recorder: AVAudioRecorder!
    private var audioURL: URL?
    @Published var isPlaying = false
    @Published var isRecording = false
    @Published var errorMessage: String = ""
    
    var currentRoute: AVAudioSessionRouteDescription {
        let audioSession = AVAudioSession.sharedInstance()
        return audioSession.currentRoute
    }
    
    
    override init() {
        super.init()
        setupRecorder()
        setupAudioSession()
    }
    
    
    func setupRecorder() {        
        let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
        let fileURL = tempDir.appendingPathComponent("recording.wav")
        
        do {
            let settings: [String: Any] = [
                AVFormatIDKey: Int(kAudioFormatLinearPCM),
                AVLinearPCMIsNonInterleaved: false,
                AVSampleRateKey: 44_100.0,
                AVNumberOfChannelsKey: 1,
                AVLinearPCMBitDepthKey: 16
            ]
            recorder = try AVAudioRecorder(url: fileURL, settings: settings)
        } catch {
            fatalError("Unable to create audio recorder: \(error.localizedDescription)")
        }
        
        recorder.delegate = self
        recorder.prepareToRecord()
    }
    
    func setupAudioSession() {
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
            try session.setActive(true)
        } catch {
            fatalError("Failed to configure and activate session.")
        }
    }
}

extension RecordViewModel {
    func record() -> Bool {
        setupRecorder()
        let started = recorder.record()
        isRecording = true
        return started
    }
    
    // Stops recording and calls the completion callback when the recording finishes.
    func stopRecording() {
        recorder.stop()
        isRecording = false
    }
    
    func play() {
        guard let url = audioURL else { print("No recording to play"); return }

        do {
            player = try AVAudioPlayer(contentsOf: url)
            if player!.prepareToPlay() {
                player!.play()
                isPlaying = true
                player!.delegate = self
            } else {
                print("player doesn't prepare yet.")
            }
        } catch {
            print("AudioPlayerError: \(error.localizedDescription)")
            return
        }
    }
    
    func stopPlayback() {
        player?.stop()
        isPlaying = false
    }
    
    func playButtonTapped() {
        if isPlaying {
            stopPlayback()
        } else {
            play()
        }
    }
    
    func recordButtonTapped() {
        if isRecording {
            stopRecording()
        } else {
            record()
        }
    }
}

// MARK: - RecordViewModel Extensions
extension RecordViewModel: AVAudioRecorderDelegate {
    
    // The AVAudioRecorderDelegate method.
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        
        let destURL = FileManager.default.urlInDocumentsDirectory(named: "recording.wav")
        try? FileManager.default.removeItem(at: destURL)
        try? FileManager.default.copyItem(at: recorder.url, to: destURL)
        
        recorder.prepareToRecord()
        
        audioURL = destURL
    }
}

extension RecordViewModel: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        isPlaying = false
    }
}

extension FileManager {
    
    var documentsDirectory: URL {
        guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("Unable to find user's documents directory")
        }
        return url
    }
    
    func urlInDocumentsDirectory(named: String) -> URL {
        return documentsDirectory.appendingPathComponent(named)
    }
}

위 코드는 애플의 예제 코드를 기반으로 작성되었습니다.


마무리

짧은 글이 될 줄 알았는데 생각보다 길어졌네요.

총정리를 하면 다음과 같습니다.

  1. 세션을 구성한다.
  2. 녹음은 AVAudioRecorder 만들기! record()로 녹음! stop()으로 중지!
  3. 재생은 AVAudioPlayer 만들기! play()로 재생! stop()으로 중지!

애플의 예제 코드를 살펴보면 스테레오를 이용한 레코딩을 할 수 있는 방법도 있으니 보면 좋을 거에요.
전 아직 이해하진 못했지만??

아무튼 이번 글은 여기서 마치도록 하겠습니다.
추후에 공부해서 더 정리할 내용이 있다면 다른 글로 작성해 볼게요.

틀린 내용에 대한 피드백은 언제나 환영입니다! 🙏


참고

0개의 댓글