iyOmSd/Title: Swift

[Swift] AVAudioRecord, AVAudioPlayer, AVAudioEngine을 사용한 음성파일 재생, 녹음, 효과음 주기

냄수 2021. 2. 19. 00:08
반응형

음성을 다루는 프로젝트를 개발하기 앞서서 관련된 지식들을 공부해보려고

기본적인 음성재생, 음성녹음, 음성효과를 줄 수 있는 기능을 구현해봤어요

 

엄청 많은 클래스들이 있더라구요

하지만 하나를 알면 다른것들은 대충 어디에쓰일지 감이 와요 ㅎㅎ

 

이 게시글에서 다룰 타입은

import AVFoundation 을 꼭 해주시구요!

 

AVAudioRecord

AVAudioPlayer

AVAudioSession

AVAudioEngine

AVAudioPlayerNode

AVAudioFile

AVAudioPCMBuffer

AVAudioUnitTimePitch

AVAudioUnitDistortion

AVAudioUnitReverb

 

 

엄청많죠? ㅎ...

저도 하면서 엄청 헷갈렷어요..

 

음성을 듣기위해서는 샘플음성파일을 직접가져와도 상관없지만!

녹음을통해서 먼저 만들어볼거에요

 

녹음

AVAudioRecord를 사용하구요

 

plist권한을 가져와야해요!

Privacy - Microphone Usage Description 마이크 접근권한!

 

먼저 오디오 세션을 가져와야해요

AVAudioSession.sharedInstance() 를 통해서 접근 할 수 있구요

세션을 가져와야 앱이 음성의 입력과 출력의 우선권을 받게되고 사용할수 있어요

 

음성을 다룰때는 목적에따라서 AVAudioSession.Category를 설정해줘야해요

녹음과 재생 모두를 다룰꺼니까 .playAndRecord 옵션을 선택해 줄게요

 

그리고 AVAudioRecord객체를 만들어주고 파라미터로url은 파일이 저장될 위치를 넣어주면되요

session.setActive(true)를 통해서 활성화를 해줘서 잘동작하도록 만들어주고

사용하지않을땐 false를 해줘야하구요

setActive(false)를 하면 들을때는 오디오가 사라지고 녹음할땐 녹음이 되지않는거에요

 

AVAudioRecord객체에 record()를 호출하면 녹음!

stop()을 호출하면 종료!

 

요약하자면

1. 파일 경로설정, 파일명 선택

   이때 FileManager를 통한 url경로를 선택할시에 적용이 안되는 이슈가 있더라구요! 주의해주세요~

2. 오디오 세션 연결

3. 녹음시작

4. 녹음종료(파일저장)

 

녹음 간단하네요!

func record() {
    do {
        var fileURL = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        fileURL.append("/testRecord.wav")

        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playAndRecord)
        recorder = try AVAudioRecorder(url: URL(string: fileURL)!, settings: [:])
        recorder.delegate = self
        try? session.setActive(true)
        recorder.record()
    } catch(let error) {
        print("record error: \(error)")
    }
}

func stopRecord() {
    recorder.stop()
    try? AVAudioSession.sharedInstance().setActive(false)
    searchRecord()
}

func searchRecord() {
    let urlString = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!

    if let urls = try? FileManager.default.subpathsOfDirectory(atPath: urlString) {
        for path in urls {
            print("\(urlString)/\(path)")
            addAudio(URL(string: "file://\(urlString)/\(path)")!)
        }
    }
}

 

재생

AVAudioPlayer를 사용해요

파일 또는 버퍼에 있는 오디오 데이터를 재생시켜주는 객체에요

기본적인 프로퍼티로는

 

pan - 좌우 스피커의 활성도 조절

rate - 배율조절 enableRate = true와 함께 해줘야함

duration - 음성파일 재생시간

 

등 기본적인 기능들을 제공해줘요

 

 

재생은 아주아주 간단해요

AVAudioPlayer객체를 만들고

생성자 파라미터로 data, url 타입이 있는데

원하는 것을 넣어주고

 .play() 하면 재생!

.stop()하면 정지!

 

func playAudio(_ audio: AudioModel.Audio) {
    if let index = audios.firstIndex(matching: audio) {
        let data = audios[index].audio
        player = try? AVAudioPlayer(data: data)
        player?.play()
    }
}

 

여기까지해서 음성을 들어보면 잘 들릴거에요

하지만!

이어폰을 끼고 들어보면 안들리고

아이폰에서 음성이 나온다는 사실을 알 수 있죠!

 

output device의 설정마저도 개발자의 몫이라는 사실..!

위에서 생성한 오디오세션에서 그 작업을 처리할 수 있구요

// 현재 입출력되는 디바이스 확인가능
print(session.currentRoute)

// 원하는 기기 옵션 설정해주기
try? session.setCategory(.playback, options: [.allowAirPlay,.allowBluetooth,.defaultToSpeaker])

airPlay, bluetooth, speaker 모드호환성을 설정해줬구요

에어팟을 끼고있다면 이제 에어팟에 음성이 들리는걸 확인할 수 있어요

 

 

음성변조

AVAudioEngine을 사용하구요

공부하면서도 제일 복잡했던 부분이라고 생각해요

 

재생과 녹음은 위의 글만 봐도 구현가능해요

 

이글은 특수효과를 위한 기능이에요

관심이없다면 안 보셔도 괜찮습니다!

 

오디오 신호를 생성하고 처리하고 오디오 입출력을 수행하는 사용되는 연결된 오디오 노드 개체 그룹.

오디오 노드를 별도로 만들어 오디오 엔진에 attach한다

런타임 동안 오디오 노드에 대한 모든 작업을 사소한 제한만으로 수행할 수 있습니다.

 

번역을 이상하게했나 크흠..  잘읽어봐도 잘이해가안가더라구요,,,

 

직접 해보면서 느끼는게 제일 와닿을거 같긴한데 아래 그림을 보면 이해가좀 되더라구요

player에 effect를 추가하고 Mixer에 효과가 추가된 최종음성이 있겟네요

 

 

기존의 음성에 효과를 추가해야하니까

기존음성이 먼저 있어야겠네요

 

AVAudioFile를 통해서 녹음한 파일 혹은 음성파일을 가져오고

AVAudioPlayerNode를 만들어주세요

이 노드는 버퍼나 파일의 schedule을 도와줘요 여기서말하는 schedule은 음성에 어떠한 작업을 하는 것 같구요

AVAudioPlayerNode는 AVAudioNode를 상속받고

재생에 특화된 녀석같죠?

 

위에서사용한 AVAudioPlayer를 사용하지 않고 왜 이걸사용하나면 

음성파일자체를 재생하는것과 수정할 수 있는 오디오데이터의 차이가 있을 것 같네요

AVAudioPlayerNodeschedule가 지원돼서 기존음성에 작업을 추가로 할 수 있죠

 

AVAudioPCMBuffer를 만들어줘야해요

우선 이 방식을 사용하지않고 그냥 AVAudioFile만으로도 할 수 있어요 이작업은 원하시면 해주세용

 

AVAudioFIle과 AVAudioPCMBuffer는 

음성파일을 읽고 쓰는 비슷한 작업을 하기때문에 헷갈릴 수 있어요

 

음성파일을 버퍼에 담고 음성에 효과를 주고 그 버퍼에 있는 음성을 듣는 방식을 해보려고 한번 해본거에요

이 방식을 사용한다면 원하는 음성포멧에 맞는 버퍼를 사용할 수 있어요

아무튼 써볼거니까..!

 

파일을 버퍼에 읽어오고

생성시 꼭 processingFormat 포멧을 선택해주세요 아니면 에러가나더라구요

// 기존 음성파일불러오기 
func setEffectFile() {
    do {
        audioFile = try AVAudioFile(forReading: recorder.url)
        playerNode = AVAudioPlayerNode()
        buffer = AVAudioPCMBuffer(pcmFormat: audioFile!.processingFormat,
                                  frameCapacity: AVAudioFrameCount(audioFile!.length))
        try audioFile?.read(into: buffer)
        recordEffect()
    } catch(let error) {
        print(error)
    }
}

 

버퍼를 사용하고 안하고의 차이는

playerNode.scheduleBuffer(buffer)
playerNode.scheduleFile(audioFile, at: nil)

뒤에서 노드의 schedule방식에 있어서 차이가나요

 

 

효과

음성효과에는 여러개가 있지만 대표적으로 몇개만 소개할게요

 

AVAudioUnit 

AVAudioNode를 상속받고있구요

오디오 유닛의 유형에 따라 실시간으로 또는 비실시간으로 오디오를 처리하는 AVAudioNode 하위 클래스.

 

이클래스를 상속받은 AVAudioUnitEffect

오디오를 실시간으로 처리하는 클래스입니다.

이 클래스를 상속한 효과들이구요

 

이 클래스를 상속받은 AVAudioUnitTimeEffect

오디오를 비실시간으로  처리하는 클래스입니다.

이 클래스를 상속한 효과들이구요

이러한 효과들이 나뉘어져 있구요

 

 

 

 

옥타브를 조절하는 AVAudioUnitTimePitch

다양한 효과가 들어있고 대표적으로 에코가 들어있는 AVAudioUnitDistortion

공간효과를 조절하는 AVAudioUnitReverb

 

각 객체를 생성해주고

AVAudioEngine에 attach시켜주고

connect를 통해서 PlayerNode - Pitch - Echo - Reverb를 연결 시켜줘야해요

마지막에는 꼭 engine.mainMixerNode에 연결시켜주고요

이렇게해야만 기존음성에 pitch를 더하고 그 음성에 echo를 더하고 ... 이렇게 잘 적용되요

 

무슨말이냐면..

playerNode - mainMixerNode

echo - mainMixerNode

pitch - mainMixerNode 이와같이 mainMixerNode로만 연결하면 안돼요..!

func recordEffect() {
    engine.stop()
    playerNode.stop()

    let unitTimePitch = AVAudioUnitTimePitch()
    unitTimePitch.pitch = 1200

    let unitEcho = AVAudioUnitDistortion()
    unitEcho.loadFactoryPreset(.multiEcho2)

    let unitReverb = AVAudioUnitReverb()
    unitReverb.loadFactoryPreset(.largeHall)

    // 재생될 노드 엔진에 추가
    engine.attach(playerNode)
    engine.attach(unitTimePitch)
    engine.attach(unitEcho)
    engine.attach(unitReverb)

    // 노드 연결하기
    engine.connect(playerNode, to: unitEcho, format: buffer!.format)
    engine.connect(unitEcho, to: unitTimePitch, format: buffer!.format)
    engine.connect(unitTimePitch, to: unitReverb, format: buffer!.format)
    engine.connect(unitReverb, to: engine.mainMixerNode, format: buffer!.format)


    if let audioFile = audioFile {
        // 후처리된 음성 준비작업
        playerNode.scheduleBuffer(buffer)

        // 시작
        try? engine.start()
        playerNode.play()
    }
}

 

 

 

 

 

 

반응형