Files
2021-08-12 16:58:38 -07:00

244 lines
8.6 KiB
Swift

//
// AudioEngine.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-01-29.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
import AVFoundation
protocol AudioEngineProtocol {
var key: Key { get }
var engine: AVAudioEngine! { get }
func play()
func pause()
func seek(toNeedle needle: Needle)
func invalidate()
}
protocol AudioEngineDelegate: AnyObject {
func didError()
}
class AudioEngine: AudioEngineProtocol {
weak var delegate:AudioEngineDelegate?
var key:Key
var engine: AVAudioEngine!
var playerNode: AVAudioPlayerNode!
private var engineInvalidated: Bool = false
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
var state:TimerState = .suspended
enum TimerState {
case suspended
case resumed
}
var needle: Needle = -1 {
didSet {
if needle >= 0 && oldValue != needle {
AudioClockDirector.shared.needleTick(key, needle: needle)
}
}
}
var duration: Duration = -1 {
didSet {
if duration >= 0 && oldValue != duration {
AudioClockDirector.shared.durationWasChanged(key, duration: duration)
}
}
}
var playingStatus: SAPlayingStatus? = nil {
didSet {
guard playingStatus != oldValue, let status = playingStatus else {
return
}
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
}
}
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false) {
didSet {
if bufferedSeconds.startingNeedle == 0.0 && bufferedSeconds.durationLoadedByNetwork == 0.0 {
bufferedSecondsDebouncer = bufferedSeconds
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
return
}
if bufferedSeconds.startingNeedle == oldValue.startingNeedle && bufferedSeconds.durationLoadedByNetwork == oldValue.durationLoadedByNetwork {
return
}
if bufferedSeconds.durationLoadedByNetwork - DEBOUNCING_BUFFER_TIME < bufferedSecondsDebouncer.durationLoadedByNetwork {
Log.debug("skipping pushing buffer: \(bufferedSeconds)")
return
}
bufferedSecondsDebouncer = bufferedSeconds
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
}
}
private var audioModifiers: [AVAudioUnit]?
init(url: AudioURL, delegate:AudioEngineDelegate?, engineAudioFormat: AVAudioFormat) {
self.key = url.key
self.delegate = delegate
engine = AVAudioEngine()
playerNode = AVAudioPlayerNode()
initHelper(engineAudioFormat)
}
func initHelper(_ engineAudioFormat: AVAudioFormat) {
engine.attach(playerNode)
audioModifiers = SAPlayer.shared.audioModifiers
defer { engine.prepare() }
guard let audioModifiers = audioModifiers, audioModifiers.count > 0 else {
engine.connect(playerNode, to: engine.mainMixerNode, format: engineAudioFormat)
return
}
audioModifiers.forEach { engine.attach($0) }
var i = 0
let node = audioModifiers[i]
engine.connect(playerNode, to: node, format: engineAudioFormat)
i += 1
while i < audioModifiers.count {
let lastNode = audioModifiers[i - 1]
let currNode = audioModifiers[i]
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
i += 1
}
let finalNode = audioModifiers[audioModifiers.count - 1]
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
}
deinit {
if state == .resumed {
playerNode.stop()
engine.stop()
}
engine.disconnectNodeInput(self.playerNode)
engine.detach(self.playerNode)
engine = nil
playerNode = nil
Log.info("deinit AVAudioEngine for \(key)")
}
func doRepeatedly(timeInterval: Double, _ closure: @escaping () -> ()) {
// A common error in AVAudioEngine is 'required condition is false: nil == owningEngine || GetEngine() == owningEngine'
// where there can only be one instance of engine running at a time and if there is already one when trying to start
// a new one then this error will be thrown.
// To handle this error we need to make sure we properly dispose of the engine when done using. In the case of timers, a
// repeating timer will maintain a strong reference to the body even if you state that you wanted a weak reference to self
// to mitigate this for repeating timers, you can either call timer.invalidate() properly or don't use repeat block timers.
// To be in better control of references and to mitigate any unforeseen issues, I decided to implement a recurisive version
// of the repeat block timer so I'm in full control of when to invalidate.
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] (timer: Timer) in
guard let self = self else { return }
guard !self.engineInvalidated else {
self.delegate = nil
return
}
closure()
self.doRepeatedly(timeInterval: timeInterval, closure)
}
}
func updateIsPlaying() {
if !bufferedSeconds.isPlayable {
if bufferedSeconds.reachedEndOfAudio(needle: needle) {
playingStatus = .ended
} else {
playingStatus = .buffering
}
return
}
let isPlaying = engine.isRunning && playerNode.isPlaying
playingStatus = isPlaying ? .playing : .paused
}
func play() {
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
if !(engine.isRunning) {
do {
try engine.start()
} catch let error {
Log.monitor(error.localizedDescription)
}
}
playerNode.play()
if state == .suspended {
state = .resumed
}
}
func pause() {
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
playerNode.pause()
engine.pause()
if state == .resumed {
state = .suspended
}
}
func seek(toNeedle needle: Needle) {
fatalError("No implementation for seek inAudioEngine, should be using streaming or disk type")
}
func invalidate() {
engineInvalidated = true
playerNode.stop()
engine.stop()
if let audioModifiers = audioModifiers, audioModifiers.count > 0 {
audioModifiers.forEach { engine.detach($0) }
}
}
}