음의 pitch값을 조정, 재생 속도를 조절할때는 AVAudioEngine을 사용한다. AVAudioEngine은 AVAudioPlayerNode로 음원파일을 재생시키고, MixerNode, AVAudioUnitVarispeed, AVAudioUnitTimePitch 노드를 추가해 사용자가 원하는 형태로 음원을 조정할 수 있다.
AVAudioPlayerNode의 scheduleFile에는 AVAudioPlayerNodeCompletionCallbackType이라는 completionHandler가 있습니다. 이 핸들러는 재생되고 있는 음원파일이 재생이 끝나면 핸들러 내부의 코드를 실행합니다. 처음에는 단순히 여기에서 PlayerNode를 stop을 하고 다시 예약을 하면 되는게 아닐까 하고 개발을 했지만, deadLock문제로 인해 내부에서는 stop를 사용할 수 없었습니다. 그래서 pause를 해놓고, 다시 scheduleFile로 예약을 하니 제가 의도했던대로 작동은 했습니다. 그러나 pause를 할 경우 playerNode는 sampleTime을 초기화를 시키지 않고, 그대로 이어 나갑니다.
이런경우, 현재 재생되고 있는 프레임의 위치를 정확하게 알수가 없습니다. 한번이상 반복하면 음원의 프레임 길이보다 현재 위치의 프레임이 더 커지는 문제가 발생합니다. 따라서 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
'iOS > 앱 개발' 카테고리의 다른 글
swift ) 현재 시간을 기준으로 특정 시간과의 차이 계산하기 (0) | 2022.08.08 |
---|---|
IOS에서 POP, OOP 어떤것을 써야 하는가! 그것이 문제로다 (0) | 2022.08.05 |
swift 캡슐화, 은닉화 (0) | 2022.07.28 |
테이블뷰에서 NSCache 사용하기 (0) | 2022.07.21 |
AVPlayer, AVAudioPlayer, AVAudioEngine, AVAudioSession (0) | 2022.07.06 |