Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/vp-resolved-session-mode
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="changed" "Audio session mode now follows the voice processing state resolved by the audio device module"
10 changes: 6 additions & 4 deletions Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate
return entryPoint?.engineDidCreate(engine) ?? 0
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, willEnableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool, isVoiceProcessingEnabled _: Bool) -> Int {
// isVoiceProcessingEnabled is new in the ADM delegate (webrtc-sdk
// PR 275). Ignored for now, exposing it through AudioEngineObserver
// is a separate API addition.
func audioDeviceModule(_: LKRTCAudioDeviceModule, willEnableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool, isVoiceProcessingEnabled: Bool) -> Int {
guard let audioManager else { return 0 }
#if os(iOS) || os(visionOS) || os(tvOS)
// The session observer configures the AVAudioSession during this transition, so hand it
// the voice processing state the ADM resolved before the chain runs.
audioManager.audioSession.setPlatformVoiceProcessingActive(isVoiceProcessingEnabled)
#endif
let entryPoint = audioManager.buildEngineObserverChain()
return entryPoint?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled) ?? 0
}
Expand Down
23 changes: 13 additions & 10 deletions Sources/LiveKit/Audio/AudioSessionEngineObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ public class AudioSessionEngineObserver: AudioEngineObserver, Loggable, @uncheck
var isAutomaticDeactivationEnabled: Bool = true
var isSpeakerOutputPreferred: Bool = true

// Whether the next capture is expected to use Apple's voice processing
// path. Updated by AudioManager before recording starts, since the ADM
// state is not yet committed when engineWillEnable fires.
var isPlatformVoiceProcessingExpected: Bool = true
// Whether the current capture uses Apple's voice processing path, as
// resolved by the ADM and delivered through the willEnableEngine
// delegate callback before the session is configured. Defaults to true
// to keep the chat-mode presets until the first transition reports.
var isPlatformVoiceProcessingActive: Bool = true

var sessionRequirements: [UUID: SessionRequirement] = [:]
}
Expand All @@ -92,7 +93,7 @@ public class AudioSessionEngineObserver: AudioEngineObserver, Loggable, @uncheck
_state.onDidMutate = { [weak self] new, old in
guard let self,
new.isSpeakerOutputPreferred != old.isSpeakerOutputPreferred ||
new.isPlatformVoiceProcessingExpected != old.isPlatformVoiceProcessingExpected else { return }
new.isPlatformVoiceProcessingActive != old.isPlatformVoiceProcessingActive else { return }
do {
try configureIfNeeded(oldState: old, newState: new)
} catch {
Expand All @@ -101,10 +102,12 @@ public class AudioSessionEngineObserver: AudioEngineObserver, Loggable, @uncheck
}
}

/// Updates the expected voice processing implementation for the next capture.
/// Called by ``AudioManager`` before recording starts or is prepared.
func setPlatformVoiceProcessingExpected(_ expected: Bool) {
_state.mutate { $0.isPlatformVoiceProcessingExpected = expected }
/// Records the voice processing implementation the ADM resolved for the current capture.
/// Called from `AudioDeviceModuleDelegateAdapter` when the ADM reports it in `willEnableEngine`,
/// and from ``AudioManager/setPlatformVoiceProcessingAllowed(_:)`` when disallowing tears down
/// Apple Voice Processing I/O directly.
func setPlatformVoiceProcessingActive(_ active: Bool) {
_state.mutate { $0.isPlatformVoiceProcessingActive = active }
}

/// Acquires an audio session requirement handle for external ownership.
Expand Down Expand Up @@ -194,7 +197,7 @@ public class AudioSessionEngineObserver: AudioEngineObserver, Loggable, @uncheck
// when Apple voice processing provides its compensating loudness
// stage. With software processing, the media-tuned presets keep
// remote audio at media playback loudness.
let playAndRecord: AudioSessionConfiguration = if newState.isPlatformVoiceProcessingExpected {
let playAndRecord: AudioSessionConfiguration = if newState.isPlatformVoiceProcessingActive {
newState.isSpeakerOutputPreferred ? .playAndRecordSpeaker : .playAndRecordReceiver
} else {
newState.isSpeakerOutputPreferred ? .playAndRecordSpeakerMedia : .playAndRecordReceiverMedia
Expand Down
35 changes: 5 additions & 30 deletions Sources/LiveKit/Audio/Manager/AudioManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,10 @@
// Keep this var within State so it's protected by UnfairLock
public var localTracksCount: Int = 0
public var remoteTracksCount: Int = 0
public var customConfigureFunc: ConfigureAudioSessionFunc?

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, tvOS Simulator,name=Apple TV 4K (3rd generation),OS=27.0)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, tvOS Simulator,name=Apple TV 4K (3rd generation),OS=27.0)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, tvOS Simulator,name=Apple TV,OS=26.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, tvOS Simulator,name=Apple TV,OS=26.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, tvOS Simulator,name=Apple TV,OS=18.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, tvOS Simulator,name=Apple TV,OS=18.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, macOS,variant=Mac Catalyst)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, visionOS Simulator,name=Apple Vision Pro,OS=27.0)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, visionOS Simulator,name=Apple Vision Pro,OS=27.0)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, visionOS Simulator,name=Apple Vision Pro,OS=26.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, visionOS Simulator,name=Apple Vision Pro,OS=26.5)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, iOS Simulator,name=iPhone 17 Pro,OS=27.0)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, iOS Simulator,name=iPhone 17 Pro,OS=27.0)

'ConfigureAudioSessionFunc' is deprecated [#DeprecatedDeclaration]

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'ConfigureAudioSessionFunc' is deprecated

Check warning on line 177 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'ConfigureAudioSessionFunc' is deprecated
public var sessionConfiguration: AudioSessionConfiguration?

public var trackState: TrackState {

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, tvOS Simulator,name=Apple TV 4K (3rd generation),OS=27.0)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, tvOS Simulator,name=Apple TV 4K (3rd generation),OS=27.0)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, tvOS Simulator,name=Apple TV,OS=26.5)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, tvOS Simulator,name=Apple TV,OS=26.5)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, tvOS Simulator,name=Apple TV,OS=18.5)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-15, 16.4, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, macOS,variant=Mac Catalyst)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, macOS,variant=Mac Catalyst)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, visionOS Simulator,name=Apple Vision Pro,OS=27.0)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, visionOS Simulator,name=Apple Vision Pro,OS=27.0)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, visionOS Simulator,name=Apple Vision Pro,OS=26.5)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, iOS Simulator,name=iPhone 17 Pro,OS=27.0)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (xcode-27, latest, iOS Simulator,name=iPhone 17 Pro,OS=27.0)

'TrackState' is deprecated [#DeprecatedDeclaration]

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'TrackState' is deprecated

Check warning on line 180 in Sources/LiveKit/Audio/Manager/AudioManager.swift

View workflow job for this annotation

GitHub Actions / Build & Test (macos-26, 26.6, iOS Simulator,name=iPhone 17 Pro,OS=26.5, true)

'TrackState' is deprecated
switch (localTracksCount > 0, remoteTracksCount > 0) {
case (true, false): .localOnly
case (false, true): .remoteOnly
Expand Down Expand Up @@ -335,13 +335,12 @@
let result = RTC.audioDeviceModule.setPlatformVoiceProcessingAllowed(allowed)
try checkAdmResult(code: result)
#if os(iOS) || os(visionOS) || os(tvOS)
// Disallowing the platform path tears down any current VPIO and makes
// future captures resolve to software processing regardless of their
// options, so the session must not keep the chat mode expectation.
// Re-allowing does not enable VPIO by itself, the next capture's
// options decide, so the expectation is left for that path to update.
// Disallowing the platform path tears down any current VPIO without an
// engine transition, so the resolved state is pushed here directly.
// Re-allowing does not enable VPIO by itself, the next transition
// reports the state it resolves.
if !allowed {
audioSession.setPlatformVoiceProcessingExpected(false)
audioSession.setPlatformVoiceProcessingActive(false)
}
#endif
}
Expand Down Expand Up @@ -432,9 +431,6 @@
_ enabled: Bool,
audioProcessingOptions: AudioProcessingOptions? = nil,
) async throws {
if enabled {
updateExpectedPlatformVoiceProcessing(for: audioProcessingOptions)
}
let result = RTC.audioDeviceModule.setRecordingAlwaysPreparedMode(
enabled,
audioProcessingOptions: audioProcessingOptions?.toRTCType(),
Expand All @@ -445,7 +441,6 @@
/// Starts mic input to the SDK even without any ``Room`` or a connection.
/// Audio buffers will flow into ``LocalAudioTrack/add(audioRenderer:)`` and ``capturePostProcessingDelegate``.
public func startLocalRecording(audioProcessingOptions: AudioProcessingOptions? = nil) throws {
updateExpectedPlatformVoiceProcessing(for: audioProcessingOptions)
// Always unmute APM if muted by last session.
RTC.audioProcessingModule.isMuted = false // TODO: Possibly not required anymore with new libs
// Start recording on the ADM.
Expand Down Expand Up @@ -580,26 +575,6 @@
}

extension AudioManager {
/// Tells the session observer which voice processing implementation the
/// next capture resolves to, before the ADM engine transition starts. The
/// session category and mode are configured during that transition, so the
/// expectation must be known up front. Also called from
/// ``LocalAudioTrack/setAudioProcessingOptions(_:)`` since track-level
/// requests reach the ADM through the sender, bypassing this manager.
func updateExpectedPlatformVoiceProcessing(for options: AudioProcessingOptions?) {
#if os(iOS) || os(visionOS) || os(tvOS)
// nil requests no processing change and the ADM keeps its current
// voice processing state, so the expectation must stay unchanged too.
guard let options else { return }
#if targetEnvironment(simulator)
let expected = false
#else
let expected = options.requestsPlatformEchoNoisePath && isPlatformVoiceProcessingAllowed
#endif
audioSession.setPlatformVoiceProcessingExpected(expected)
#endif
}

func buildEngineObserverChain() -> (any AudioEngineObserver)? {
var objects = _state.engineObservers
guard !objects.isEmpty else { return nil }
Expand Down
7 changes: 1 addition & 6 deletions Sources/LiveKit/Track/Local/LocalAudioTrack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,7 @@ public class LocalAudioTrack: Track, LocalTrackProtocol, AudioTrackProtocol, @un
message: "Media track is not an audio track",
)
}
let result = try audioTrack.setAudioProcessingOptions(options.toRTCType()).toLKType()
// Track-level options reach the ADM through the sender, so inform the
// session observer here to keep the session mode in sync with the
// requested voice processing implementation.
AudioManager.shared.updateExpectedPlatformVoiceProcessing(for: options)
return result
return try audioTrack.setAudioProcessingOptions(options.toRTCType()).toLKType()
}

// MARK: - Internal
Expand Down
12 changes: 0 additions & 12 deletions Sources/LiveKit/Types/AudioProcessingOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,6 @@ public struct AudioProcessingOptions: Hashable, Sendable {
}
}

extension AudioProcessingOptions {
/// Whether these options request Apple's coupled voice processing path.
///
/// Mirrors the coupled AEC/NS resolution: any enabled echo cancellation or
/// noise suppression component that is not forced to software prefers the
/// platform path when it is available.
var requestsPlatformEchoNoisePath: Bool {
(echoCancellation && echoCancellationMode != .software) ||
(noiseSuppression && noiseSuppressionMode != .software)
}
}

/// The caller's request for one audio processing component: enabled flag plus
/// implementation mode.
public struct AudioProcessingComponentRequest<Mode: Sendable>: Sendable {
Expand Down
43 changes: 0 additions & 43 deletions Tests/LiveKitCoreTests/AudioProcessingOptionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,47 +88,4 @@ struct AudioProcessingOptionsTests {
#expect(echoCancellation.mode == .platform)
#expect(highpassFilter.mode == .software)
}

@Test func defaultOptionsRequestPlatformEchoNoisePath() {
#expect(AudioProcessingOptions().requestsPlatformEchoNoisePath == true)
}

@Test func softwareModesDoNotRequestPlatformEchoNoisePath() {
let options = AudioProcessingOptions(
echoCancellationMode: .software,
autoGainControlMode: .software,
noiseSuppressionMode: .software,
)
#expect(options.requestsPlatformEchoNoisePath == false)
}

@Test func singleNonSoftwareComponentRequestsPlatformEchoNoisePath() {
let options = AudioProcessingOptions(
echoCancellationMode: .software,
noiseSuppressionMode: .automatic,
)
#expect(options.requestsPlatformEchoNoisePath == true)
}

@Test func disabledComponentsDoNotRequestPlatformEchoNoisePath() {
#expect(AudioProcessingOptions.noProcessing.requestsPlatformEchoNoisePath == false)

let disabledButPlatformMode = AudioProcessingOptions(
echoCancellation: false,
noiseSuppression: false,
echoCancellationMode: .platform,
noiseSuppressionMode: .platform,
)
#expect(disabledButPlatformMode.requestsPlatformEchoNoisePath == false)
}

@Test func agcAloneDoesNotRequestPlatformEchoNoisePath() {
let options = AudioProcessingOptions(
echoCancellation: false,
autoGainControl: true,
noiseSuppression: false,
autoGainControlMode: .platform,
)
#expect(options.requestsPlatformEchoNoisePath == false)
}
}
Loading