@@ -13,27 +13,45 @@ import StreamWebRTC
1313/// audio pipeline can stay in sync with application logic.
1414final class AudioDeviceModule : NSObject , RTCAudioDeviceModuleDelegate , Encodable , @unchecked Sendable {
1515
16+ /// Helper constants used across the module.
1617 enum Constant {
17- // WebRTC interfaces are returning integer result codes. We use this typed/named
18- // constant to define the Success of an operation.
18+ /// WebRTC interfaces return integer result codes. We use this typed/named
19+ /// constant to define the success of an operation.
1920 static let successResult = 0
2021
21- // The down limit of audio pipeline in DB that is considered silence.
22+ /// Audio pipeline floor in dB that we interpret as silence.
2223 static let silenceDB : Float = - 160
2324 }
2425
2526 /// Events emitted as the underlying audio engine changes state.
2627 enum Event : Equatable , CustomStringConvertible {
28+ /// Outbound audio surpassed the silence threshold.
2729 case speechActivityStarted
30+ /// Outbound audio dropped back to silence.
2831 case speechActivityEnded
32+ /// A new `AVAudioEngine` instance has been created.
2933 case didCreateAudioEngine( AVAudioEngine )
34+ /// The engine is about to enable playout/recording paths.
3035 case willEnableAudioEngine( AVAudioEngine , isPlayoutEnabled: Bool , isRecordingEnabled: Bool )
36+ /// The engine is about to start rendering.
3137 case willStartAudioEngine( AVAudioEngine , isPlayoutEnabled: Bool , isRecordingEnabled: Bool )
38+ /// The engine has fully stopped.
3239 case didStopAudioEngine( AVAudioEngine , isPlayoutEnabled: Bool , isRecordingEnabled: Bool )
40+ /// The engine was disabled after stopping.
3341 case didDisableAudioEngine( AVAudioEngine , isPlayoutEnabled: Bool , isRecordingEnabled: Bool )
42+ /// The engine will be torn down.
3443 case willReleaseAudioEngine( AVAudioEngine )
44+ /// The input graph is configured with a new source node.
3545 case configureInputFromSource( AVAudioEngine , source: AVAudioNode ? , destination: AVAudioNode , format: AVAudioFormat )
46+ /// The output graph is configured with a destination node.
3647 case configureOutputFromSource( AVAudioEngine , source: AVAudioNode , destination: AVAudioNode ? , format: AVAudioFormat )
48+ /// Voice processing knobs changed.
49+ case didUpdateAudioProcessingState(
50+ voiceProcessingEnabled: Bool ,
51+ voiceProcessingBypassed: Bool ,
52+ voiceProcessingAGCEnabled: Bool ,
53+ stereoPlayoutEnabled: Bool
54+ )
3755
3856 var description : String {
3957 switch self {
@@ -66,52 +84,92 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
6684
6785 case . configureOutputFromSource( let engine, let source, let destination, let format) :
6886 return " .configureOutputFromSource( \( engine) , source: \( source) , destination: \( destination) , format: \( format) ) "
87+
88+ case let . didUpdateAudioProcessingState(
89+ voiceProcessingEnabled,
90+ voiceProcessingBypassed,
91+ voiceProcessingAGCEnabled,
92+ stereoPlayoutEnabled
93+ ) :
94+ return " .didUpdateAudioProcessingState(voiceProcessingEnabled: \( voiceProcessingEnabled) , voiceProcessingBypassed: \( voiceProcessingBypassed) , voiceProcessingAGCEnabled: \( voiceProcessingAGCEnabled) , stereoPlayoutEnabled: \( stereoPlayoutEnabled) ) "
6995 }
7096 }
7197 }
7298
99+ /// Tracks whether WebRTC is currently playing back audio.
73100 private let isPlayingSubject : CurrentValueSubject < Bool , Never >
101+ /// `true` while audio playout is active.
74102 var isPlaying : Bool { isPlayingSubject. value }
103+ /// Publisher that reflects playout activity changes.
75104 var isPlayingPublisher : AnyPublisher < Bool , Never > { isPlayingSubject. eraseToAnyPublisher ( ) }
76105
106+ /// Tracks whether WebRTC is capturing microphone samples.
77107 private let isRecordingSubject : CurrentValueSubject < Bool , Never >
108+ /// `true` while audio capture is active.
78109 var isRecording : Bool { isRecordingSubject. value }
110+ /// Publisher that reflects recording activity changes.
79111 var isRecordingPublisher : AnyPublisher < Bool , Never > { isRecordingSubject. eraseToAnyPublisher ( ) }
80112
113+ /// Tracks whether the microphone is muted at the ADM layer.
81114 private let isMicrophoneMutedSubject : CurrentValueSubject < Bool , Never >
115+ /// `true` if the microphone is muted.
82116 var isMicrophoneMuted : Bool { isMicrophoneMutedSubject. value }
117+ /// Publisher that reflects microphone mute changes.
83118 var isMicrophoneMutedPublisher : AnyPublisher < Bool , Never > { isMicrophoneMutedSubject. eraseToAnyPublisher ( ) }
84119
120+ /// Tracks whether stereo playout is configured.
85121 private let isStereoPlayoutEnabledSubject : CurrentValueSubject < Bool , Never >
122+ /// `true` if stereo playout is available and active.
86123 var isStereoPlayoutEnabled : Bool { isStereoPlayoutEnabledSubject. value }
124+ /// Publisher emitting stereo playout state.
87125 var isStereoPlayoutEnabledPublisher : AnyPublisher < Bool , Never > { isStereoPlayoutEnabledSubject. eraseToAnyPublisher ( ) }
88126
127+ /// Tracks whether VP processing is currently bypassed.
89128 private let isVoiceProcessingBypassedSubject : CurrentValueSubject < Bool , Never >
129+ /// `true` if the voice processing unit is bypassed.
90130 var isVoiceProcessingBypassed : Bool { isVoiceProcessingBypassedSubject. value }
131+ /// Publisher emitting VP bypass changes.
91132 var isVoiceProcessingBypassedPublisher : AnyPublisher < Bool , Never > { isVoiceProcessingBypassedSubject. eraseToAnyPublisher ( ) }
92133
134+ /// Tracks whether voice processing is enabled.
93135 private let isVoiceProcessingEnabledSubject : CurrentValueSubject < Bool , Never >
136+ /// `true` when Apple VP is active.
94137 var isVoiceProcessingEnabled : Bool { isVoiceProcessingEnabledSubject. value }
138+ /// Publisher emitting VP enablement changes.
95139 var isVoiceProcessingEnabledPublisher : AnyPublisher < Bool , Never > { isVoiceProcessingEnabledSubject. eraseToAnyPublisher ( ) }
96140
141+ /// Tracks whether automatic gain control is enabled inside VP.
97142 private let isVoiceProcessingAGCEnabledSubject : CurrentValueSubject < Bool , Never >
143+ /// `true` while AGC is active.
98144 var isVoiceProcessingAGCEnabled : Bool { isVoiceProcessingAGCEnabledSubject. value }
145+ /// Publisher emitting AGC changes.
99146 var isVoiceProcessingAGCEnabledPublisher : AnyPublisher < Bool , Never > { isVoiceProcessingAGCEnabledSubject. eraseToAnyPublisher ( ) }
100147
148+ /// Observes RMS audio levels (in dB) derived from the input tap.
101149 private let audioLevelSubject = CurrentValueSubject < Float , Never > ( Constant . silenceDB) // default to silence
150+ /// Latest measured audio level.
102151 var audioLevel : Float { audioLevelSubject. value }
152+ /// Publisher emitting audio level updates.
103153 var audioLevelPublisher : AnyPublisher < Float , Never > { audioLevelSubject. eraseToAnyPublisher ( ) }
104154
155+ /// Wrapper around WebRTC `RTCAudioDeviceModule`.
105156 private let source : any RTCAudioDeviceModuleControlling
157+ /// Manages Combine subscriptions generated by this module.
106158 private let disposableBag : DisposableBag = . init( )
107159
160+ /// Serial queue used to deliver events to observers.
108161 private let dispatchQueue : DispatchQueue
162+ /// Internal relay that feeds `publisher`.
109163 private let subject : PassthroughSubject < Event , Never >
164+ /// Object that taps engine nodes and publishes audio level data.
110165 private var audioLevelsAdapter : AudioEngineNodeAdapting
166+ /// Public stream of `Event` values describing engine transitions.
111167 let publisher : AnyPublisher < Event , Never >
112168
169+ /// Strong reference to the current engine so we can introspect it if needed.
113170 private var engine : AVAudioEngine ?
114171
172+ /// Textual diagnostics for logging and debugging.
115173 override var description : String {
116174 " { " +
117175 " isPlaying: \( isPlaying) " +
@@ -159,14 +217,20 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
159217
160218 audioLevelsAdapter. subject = audioLevelSubject
161219 source. observer = self
220+
221+ source. isVoiceProcessingBypassed = true
162222 }
163223
164224 // MARK: - Recording
165225
226+ /// Reinitializes the ADM, clearing its internal audio graph state.
166227 func reset( ) {
167228 _ = source. reset ( )
168229 }
169230
231+ /// Switches between stereo and mono playout while keeping the recording
232+ /// state consistent across reinitializations.
233+ /// - Parameter isPreferred: `true` when stereo output should be used.
170234 func setStereoPlayoutPreference( _ isPreferred: Bool ) {
171235 /// - Important: `.voiceProcessing` requires VP to be enabled in order to mute and
172236 /// `.restartEngine` rebuilds the whole graph. Each of them has different issues:
@@ -189,11 +253,18 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
189253 _ = source. stopRecording ( )
190254 _ = source. initAndStartRecording ( )
191255
256+ if isPreferred {
257+ setVoiceProcessingBypassed ( isPreferred)
258+ }
259+
192260 if isMuted {
193261 _ = source. setMicrophoneMuted ( isMuted)
194262 }
195263 }
196264
265+ /// Starts or stops speaker playout on the ADM, retrying transient failures.
266+ /// - Parameter isActive: `true` to start playout, `false` to stop.
267+ /// - Throws: `ClientError` when WebRTC returns a non-zero status.
197268 func setPlayout( _ isActive: Bool ) throws {
198269 try RetriableTask . run ( iterations: 3 ) {
199270 try throwingExecution ( " Unable to start playout " ) {
@@ -245,19 +316,29 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
245316 return
246317 }
247318
319+ try throwingExecution ( " Unable to initAndStartRecording for setMicrophoneMuted: \( isMuted) " ) {
320+ source. initAndStartRecording ( )
321+ }
322+
248323 try throwingExecution ( " Unable to setMicrophoneMuted: \( isMuted) " ) {
249324 source. setMicrophoneMuted ( isMuted)
250325 }
251326
252327 isMicrophoneMutedSubject. send ( isMuted)
253328 }
254329
330+ /// Forces the ADM to recompute whether stereo output is supported.
255331 func refreshStereoPlayoutState( ) {
256332 source. refreshStereoPlayoutState ( )
257333 }
258334
335+ func setVoiceProcessingBypassed( _ value: Bool ) {
336+ source. isVoiceProcessingBypassed = value
337+ }
338+
259339 // MARK: - RTCAudioDeviceModuleDelegate
260340
341+ /// Receives speech activity notifications emitted by WebRTC VAD.
261342 func audioDeviceModule(
262343 _ audioDeviceModule: RTCAudioDeviceModule ,
263344 didReceiveSpeechActivityEvent speechActivityEvent: RTCSpeechActivityEvent
@@ -272,6 +353,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
272353 }
273354 }
274355
356+ /// Stores the created engine reference and emits an event so observers can
357+ /// hook into the audio graph configuration.
275358 func audioDeviceModule(
276359 _ audioDeviceModule: RTCAudioDeviceModule ,
277360 didCreateEngine engine: AVAudioEngine
@@ -281,6 +364,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
281364 return Constant . successResult
282365 }
283366
367+ /// Keeps local playback/recording state in sync as WebRTC enables the
368+ /// corresponding engine paths.
284369 func audioDeviceModule(
285370 _ audioDeviceModule: RTCAudioDeviceModule ,
286371 willEnableEngine engine: AVAudioEngine ,
@@ -299,6 +384,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
299384 return Constant . successResult
300385 }
301386
387+ /// Mirrors state when the engine is about to start running and delivering
388+ /// audio samples.
302389 func audioDeviceModule(
303390 _ audioDeviceModule: RTCAudioDeviceModule ,
304391 willStartEngine engine: AVAudioEngine ,
@@ -318,6 +405,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
318405 return Constant . successResult
319406 }
320407
408+ /// Updates state and notifies observers once the engine has completely
409+ /// stopped.
321410 func audioDeviceModule(
322411 _ audioDeviceModule: RTCAudioDeviceModule ,
323412 didStopEngine engine: AVAudioEngine ,
@@ -336,6 +425,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
336425 return Constant . successResult
337426 }
338427
428+ /// Tracks when the engine has been disabled after stopping so clients can
429+ /// react (e.g., rebuilding audio graphs).
339430 func audioDeviceModule(
340431 _ audioDeviceModule: RTCAudioDeviceModule ,
341432 didDisableEngine engine: AVAudioEngine ,
@@ -354,6 +445,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
354445 return Constant . successResult
355446 }
356447
448+ /// Clears internal references before WebRTC disposes the engine.
357449 func audioDeviceModule(
358450 _ audioDeviceModule: RTCAudioDeviceModule ,
359451 willReleaseEngine engine: AVAudioEngine
@@ -364,6 +456,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
364456 return Constant . successResult
365457 }
366458
459+ /// Keeps observers informed when WebRTC sets up the input graph and installs
460+ /// an audio level tap to monitor microphone activity.
367461 func audioDeviceModule(
368462 _ audioDeviceModule: RTCAudioDeviceModule ,
369463 engine: AVAudioEngine ,
@@ -389,6 +483,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
389483 return Constant . successResult
390484 }
391485
486+ /// Emits an event whenever WebRTC reconfigures the output graph.
392487 func audioDeviceModule(
393488 _ audioDeviceModule: RTCAudioDeviceModule ,
394489 engine: AVAudioEngine ,
@@ -408,22 +503,34 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
408503 return Constant . successResult
409504 }
410505
506+ /// Currently unused: CallKit/RoutePicker own the device selection UX.
411507 func audioDeviceModuleDidUpdateDevices(
412508 _ audioDeviceModule: RTCAudioDeviceModule
413509 ) {
414510 // No-op
415511 }
416512
513+ /// Mirrors state changes coming from CallKit/WebRTC voice-processing
514+ /// controls so UI can reflect the correct toggles.
417515 func audioDeviceModule(
418516 _ module: RTCAudioDeviceModule ,
419517 didUpdateAudioProcessingState state: RTCAudioProcessingState
420518 ) {
519+ subject. send (
520+ . didUpdateAudioProcessingState(
521+ voiceProcessingEnabled: state. voiceProcessingEnabled,
522+ voiceProcessingBypassed: state. voiceProcessingBypassed,
523+ voiceProcessingAGCEnabled: state. voiceProcessingAGCEnabled,
524+ stereoPlayoutEnabled: state. stereoPlayoutEnabled
525+ )
526+ )
421527 isVoiceProcessingEnabledSubject. send ( state. voiceProcessingEnabled)
422528 isVoiceProcessingBypassedSubject. send ( state. voiceProcessingBypassed)
423529 isVoiceProcessingAGCEnabledSubject. send ( state. voiceProcessingAGCEnabled)
424530 isStereoPlayoutEnabledSubject. send ( state. stereoPlayoutEnabled)
425531 }
426532
533+ /// Mirrors the subset of properties that can be encoded for debugging.
427534 private enum CodingKeys : String , CodingKey {
428535 case isPlaying
429536 case isRecording
@@ -436,6 +543,7 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
436543 case audioLevel
437544 }
438545
546+ /// Serializes the module state, primarily for diagnostic payloads.
439547 func encode( to encoder: Encoder ) throws {
440548 var container = encoder. container ( keyedBy: CodingKeys . self)
441549 try container. encode ( isPlaying, forKey: . isPlaying)
@@ -450,6 +558,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
450558
451559 // MARK: - Private helpers
452560
561+ /// Runs a WebRTC ADM call and translates its integer result into a
562+ /// `ClientError` enriched with call-site metadata.
453563 private func throwingExecution(
454564 _ message: @autoclosure ( ) -> String ,
455565 file: StaticString = #file,
@@ -473,6 +583,8 @@ final class AudioDeviceModule: NSObject, RTCAudioDeviceModuleDelegate, Encodable
473583
474584extension AVAudioEngine {
475585
586+ /// Human-readable description of the current output node format, used in
587+ /// logs when debugging device routing issues.
476588 var outputDescription : String {
477589 guard let remoteIO = outputNode. audioUnit else {
478590 return " not available "
0 commit comments