-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPlayolaMainMixer.swift
More file actions
197 lines (166 loc) · 5.53 KB
/
Copy pathPlayolaMainMixer.swift
File metadata and controls
197 lines (166 loc) · 5.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//
// PlayolaMainMixer.swift
// PlayolaPlayer
//
// Created by Brian D Keane on 1/6/25.
//
@preconcurrency import AVFoundation
import Foundation
import PlayolaCore
import os.log
#if os(iOS) || os(tvOS)
import UIKit
#endif
public protocol PlayolaMainMixerDelegate: AnyObject {
func player(_ mainMixer: PlayolaMainMixer, didPlayBuffer: AVAudioPCMBuffer)
}
/// Default properties for the tap
enum TapProperties {
case `default`
/// The amount of samples in each buffer of audio
var bufferSize: AVAudioFrameCount {
return 512
}
/// The format of the audio in the tap (desired is float 32, non-interleaved)
var format: AVAudioFormat {
return AVAudioFormat(
commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
}
}
open class PlayolaMainMixer: NSObject {
open private(set) var mixerNode: AVAudioMixerNode
open private(set) var engine: AVAudioEngine
open var delegate: PlayolaMainMixerDelegate?
private let errorReporter = PlayolaErrorReporter.shared
let audioSessionManager: AudioSessionManager
private static let logger = OSLog(subsystem: "fm.playola.playolaCore", category: "MainMixer")
override init() {
self.mixerNode = AVAudioMixerNode()
self.engine = AVAudioEngine()
self.audioSessionManager = AudioSessionManager()
super.init()
self.engine.attach(self.mixerNode)
self.engine.connect(
self.mixerNode,
to: self.engine.mainMixerNode,
format: TapProperties.default.format)
self.engine.prepare()
self.mixerNode.installTap(
onBus: 0,
bufferSize: TapProperties.default.bufferSize,
format: TapProperties.default.format,
block: self.onTap(_:_:))
}
deinit {
self.mixerNode.removeTap(onBus: 0)
}
/// Configures the shared audio session for playback (fire-and-forget)
public func configureAudioSession() {
guard !audioSessionManager.isConfigured else { return }
Task { @MainActor in
do {
try await audioSessionManager.configureForPlayback()
try await audioSessionManager.activate()
os_log("Audio session successfully configured", log: PlayolaMainMixer.logger, type: .info)
} catch {
let deviceName = DeviceInfoProvider.deviceName
let systemVersion = DeviceInfoProvider.systemVersion
await errorReporter.reportError(
error,
context:
"Failed to configure audio session | Device: \(deviceName) | OS: \(systemVersion)",
level: .critical)
}
}
}
/// Configures the shared audio session for playback and waits for completion.
/// Use this before engine.start() to avoid stalling the audio engine.
@MainActor
public func ensureAudioSessionConfigured() async throws {
guard !audioSessionManager.isConfigured else { return }
do {
try await audioSessionManager.configureForPlayback()
try await audioSessionManager.activate()
os_log("Audio session successfully configured", log: PlayolaMainMixer.logger, type: .info)
} catch {
let deviceName = DeviceInfoProvider.deviceName
let systemVersion = DeviceInfoProvider.systemVersion
await errorReporter.reportError(
error,
context:
"Failed to configure audio session | Device: \(deviceName) | OS: \(systemVersion)",
level: .critical)
throw error
}
}
/// Deactivates the audio session when it's no longer needed
public func deactivateAudioSession() {
guard audioSessionManager.isConfigured else { return }
Task { @MainActor in
do {
try await audioSessionManager.deactivate()
os_log("Audio session deactivated", log: PlayolaMainMixer.logger, type: .info)
} catch {
await errorReporter.reportError(
error, context: "Failed to deactivate audio session", level: .warning)
}
}
}
/// Handles the audio tap
private func onTap(_ buffer: AVAudioPCMBuffer, _ time: AVAudioTime) {
self.delegate?.player(self, didPlayBuffer: buffer)
}
@MainActor
public static let shared: PlayolaMainMixer = PlayolaMainMixer()
}
extension PlayolaMainMixer {
/// Starts the audio engine off the main thread to avoid blocking on
/// AUIOClient_StartIO during cold hardware initialization (e.g. resuming
/// from a phone-call interruption). AVAudioEngine.start() is thread-safe;
/// only the start call itself is dispatched off main.
@MainActor
public func start() async throws {
let engine = self.engine
do {
try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.global(qos: .userInitiated).async {
do {
try engine.start()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
} catch {
await errorReporter.reportError(
error, context: "Failed to start audio engine",
level: .critical)
throw error
}
}
@MainActor
public func restartEngine() async throws {
os_log("Restarting audio engine", log: PlayolaMainMixer.logger, type: .info)
if engine.isRunning {
engine.stop()
}
engine.prepare()
try await start()
}
public var isEngineRunning: Bool {
return engine.isRunning
}
public func attach(_ node: AVAudioPlayerNode) {
engine.attach(node)
}
public func connect(
_ playerNode: AVAudioPlayerNode, to mixerNode: AVAudioMixerNode, format: AVAudioFormat?
) {
engine.connect(playerNode, to: mixerNode, format: format)
}
public func prepare() {
engine.prepare()
}
}