AVAudioEngine으로 오디오 파일 재생시, 오디오 파일이 재생이 완료되면 다시 처음으로 돌아가게 하기.

음의 pitch값을 조정, 재생 속도를 조절할때는 AVAudioEngine을 사용한다. AVAudioEngine은 AVAudioPlayerNode로 음원파일을 재생시키고, MixerNode, AVAudioUnitVarispeed, AVAudioUnitTimePitch 노드를 추가해 사용자가 원하는 형태로 음원을 조정할 수 있다. 

 

AVAudioPlayerNode의 scheduleFile에는 AVAudioPlayerNodeCompletionCallbackType이라는 completionHandler가 있습니다. 이 핸들러는 재생되고 있는 음원파일이 재생이 끝나면 핸들러 내부의 코드를 실행합니다. 처음에는 단순히 여기에서 PlayerNode를 stop을 하고 다시 예약을 하면 되는게 아닐까 하고 개발을 했지만, deadLock문제로 인해 내부에서는 stop를 사용할 수 없었습니다. 그래서 pause를 해놓고, 다시 scheduleFile로 예약을 하니 제가 의도했던대로 작동은 했습니다. 그러나 pause를 할 경우 playerNode는 sampleTime을 초기화를 시키지 않고, 그대로 이어 나갑니다.

pause인 상태로 scheduleFile할시 발생하는 문제

이런경우, 현재 재생되고 있는 프레임의 위치를 정확하게 알수가 없습니다. 한번이상 반복하면 음원의 프레임 길이보다 현재 위치의 프레임이 더 커지는 문제가 발생합니다. 따라서 PlayerNode를 Stop()을 시켜야 합니다.

PlayerNode를 stop하면 stop한 시점에서 sampleTime역시 더이상 증가하지 않고, 다시 scheduleFile을 한 후에 playerNode를 play하면 sampleTime이 0으로 초기화가 됩니다. 이러한 방식으로 구현하려면 계속해서 현재 재생되고 있는 프레임 위치를 가져와서 오디오파일의 전체 프레임 길이와 비교를 해야 합니다. 

 

변수 선언

    var audioFile : AVAudioFile?
    var format : AVAudioFormat?
    var audioFileSampleRate : Double = 0
    var audioFileLengthSecond : Double = 0
    var audioLengthSamples : AVAudioFramePosition = 0
    var currentPosition : AVAudioFramePosition = 0
    var seekFrame : AVAudioFramePosition = 0
    var currentFrame : AVAudioFramePosition{
        guard let lastRenderTime = playerNode.lastRenderTime,
              let playerTime = playerNode.playerTime(forNodeTime: lastRenderTime)
        else{
            return 0
        }
        return playerTime.sampleTime
    }
    
    var isPlay = false
    

    let audioEngine = AVAudioEngine()
    let playerNode = AVAudioPlayerNode()
    var displayLink : CADisplayLink?

 

재생할 오디오파일 세팅 및 변수 초기화

func setAudioFile(){
        let fileURL = Bundle.main.url(forResource: "audioFile", withExtension: "m4a")!
        guard let file = try? AVAudioFile(forReading: fileURL) else {return}
        self.format = file.processingFormat
        self.audioFile = file
        self.audioFileSampleRate = file.processingFormat.sampleRate
        self.audioLengthSamples = file.length
        audioFileLengthSecond = Double(audioLengthSamples) / audioFileSampleRate
    }

playerNode에 오디오파일 예약

func setScheduleFile(){
        guard let file = audioFile else {return}
        playerNode.scheduleFile(file, at: nil, completionCallbackType: .dataPlayedBack)
    }

AVAudioEngine 설정 및 실행

private func setEngine(format : AVAudioFormat?){
        audioEngine.attach(playerNode)
        audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: format)
        
        self.audioEngine.prepare()
        do{
            try audioEngine.start()
            setScheduleFile()
        }catch{
            print("AUDIO ENGINE START ERROR")
        }
    }

DisplayLink 설정

func setDisplayLink(){
        displayLink = CADisplayLink(target: self, selector: #selector(updateDisplay))
        displayLink?.add(to: .current, forMode: .default)
        displayLink?.isPaused = true
    }

DisplayLink가 실행할 함수

@objc private func updateDisplay(){
        currentPosition = currentFrame
        currentPosition = max(currentPosition, 0)
        currentPosition = min(currentPosition, audioLengthSamples)
        if currentPosition >= audioLengthSamples{
            playerNode.stop()
            currentPosition = 0
            isPlay = false
            displayLink?.isPaused = true
            setScheduleFile()
        }
    }

실행영상

영상에서는 소리가 안들리지만, 오디오가 재생이 끝나고 다시 실행 시킬시 currentSampleTime이 0부터 시작한다는것을 볼 수 있습니다.

 

추가로 처음에 언급한 방식대로 구현했을 경우 scheduleFile의 AVAudioPlayerNodeCompletionCallbackType을 이용했을때 playerNode의 currentSampleTime을 출력시켜보면 정지상태일경우 0이 출력되지만 재생상태일경우 이전값부터 계속 currentSampleTime이 이어져 나가는것을 확인 할 수 있습니다.

func setScheduleFile(){
        guard let file = audioFile else {return}
        playerNode.scheduleFile(file, at: nil, completionCallbackType: .dataPlayedBack) {_ in
            self.playerNode.pause()
            self.setScheduleFile()
        }
    }

 

 

참고사이트 : https://www.raywenderlich.com/21672160-avaudioengine-tutorial-for-ios-getting-started