Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVAudioEngine version AudioDeviceModule #536

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
622caa9
Tests
hiroshihorie Jan 6, 2025
9908d8e
Update render test
hiroshihorie Jan 6, 2025
3b9bbb3
Backward compatible session config
hiroshihorie Jan 8, 2025
b1f3ae1
.mixWithOthers by default
hiroshihorie Jan 8, 2025
b1871da
Ducking config
hiroshihorie Jan 8, 2025
be593c1
Use 125.6422.12-exp.2
hiroshihorie Jan 8, 2025
89c084c
Muted speech activity
hiroshihorie Jan 8, 2025
4d3b752
Merge branch 'main' into hiroshi/adm-audioengine2
hiroshihorie Jan 13, 2025
282cbc7
Update node config methods
hiroshihorie Jan 13, 2025
8f70540
Move audio buffer
hiroshihorie Jan 13, 2025
92e3406
Update AudioManager.swift
hiroshihorie Jan 13, 2025
4b77f84
Use 125.6422.12-exp.3
hiroshihorie Jan 14, 2025
7cd4f29
Fix tests
hiroshihorie Jan 14, 2025
5e217e1
Merge branch 'main' into hiroshi/adm-audioengine2
hiroshihorie Jan 14, 2025
130e1d2
Fix tests
hiroshihorie Jan 14, 2025
ae16a3c
Merge branch 'hiroshi/adm-audioengine2' of https://github.com/livekit…
hiroshihorie Jan 14, 2025
874b3a4
AudioDuckingLevel type
hiroshihorie Jan 14, 2025
49c91ef
Use 125.6422.12-exp.4
hiroshihorie Jan 14, 2025
4b84621
Fix Xcode 14.2
hiroshihorie Jan 14, 2025
5a585a3
Change session config timing
hiroshihorie Jan 16, 2025
a0103ad
Update state tests
hiroshihorie Jan 20, 2025
256b42a
P1
hiroshihorie Jan 20, 2025
8c60160
Merge branch 'main' into hiroshi/adm-audioengine2
hiroshihorie Jan 20, 2025
68f77f3
Merge branch 'hiroshi/adm-audioengine2' of https://github.com/livekit…
hiroshihorie Jan 20, 2025
a987f77
Merge branch 'main' into hiroshi/adm-audioengine2
hiroshihorie Jan 21, 2025
81ed8c8
Merge branch 'hiroshi/adm-audioengine2' of https://github.com/livekit…
hiroshihorie Jan 21, 2025
3c9c0dd
Chained engine observer
hiroshihorie Jan 22, 2025
7e48b7b
lib 125.6422.12-exp.5
hiroshihorie Jan 22, 2025
d3deb72
Update test
hiroshihorie Jan 22, 2025
aa8977b
Fix test
hiroshihorie Jan 22, 2025
82323b4
Update manual render test
hiroshihorie Jan 23, 2025
d846a00
no processing option for capture options
hiroshihorie Jan 23, 2025
0e05ca0
Runtime agc / bypass vp toggle
hiroshihorie Jan 28, 2025
8b35ab6
Runtime bypass is valid now
hiroshihorie Jan 28, 2025
a28b9c8
Use 125.6422.12-exp.6
hiroshihorie Jan 28, 2025
9c18ff5
Fix manual render test
hiroshihorie Jan 28, 2025
89e0620
Update manual render tests
hiroshihorie Jan 29, 2025
f88456e
Use 125.6422.12
hiroshihorie Jan 29, 2025
5ad8bd5
make state sync sendable
hiroshihorie Jan 29, 2025
c5ee683
Backward compatibility for custom config func
hiroshihorie Jan 29, 2025
38c563b
Strip unused code
hiroshihorie Jan 29, 2025
ce79ac6
Refactoring
hiroshihorie Jan 29, 2025
e57a5ad
Fix tests
hiroshihorie Jan 29, 2025
107e239
Rename onMutedSpeechActivity
hiroshihorie Jan 29, 2025
6820281
Squashed commit of the following:
hiroshihorie Jan 29, 2025
77c7cfe
Use 125.6422.13
hiroshihorie Jan 29, 2025
d44a090
Disable RTC audio options by default
hiroshihorie Jan 29, 2025
3c6d6b5
Fix apm tests
hiroshihorie Jan 29, 2025
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let package = Package(
],
dependencies: [
// LK-Prefixed Dynamic WebRTC XCFramework
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.11"),
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.13"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
// Only used for DocC generation
Expand Down
2 changes: 1 addition & 1 deletion [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
],
dependencies: [
// LK-Prefixed Dynamic WebRTC XCFramework
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.11"),
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.13"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
// Only used for DocC generation
Expand Down
88 changes: 88 additions & 0 deletions Sources/LiveKit/Audio/AudioDeviceModuleDelegateAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

#if swift(>=5.9)
internal import LiveKitWebRTC
#else
@_implementationOnly import LiveKitWebRTC
#endif

// Invoked on WebRTC's worker thread, do not block.
class AudioDeviceModuleDelegateAdapter: NSObject, LKRTCAudioDeviceModuleDelegate {
weak var audioManager: AudioManager?

func audioDeviceModule(_: LKRTCAudioDeviceModule, didReceiveSpeechActivityEvent speechActivityEvent: RTCSpeechActivityEvent) {
guard let audioManager else { return }
audioManager.onMutedSpeechActivity?(audioManager, speechActivityEvent.toLKType())
}

func audioDeviceModuleDidUpdateDevices(_: LKRTCAudioDeviceModule) {
guard let audioManager else { return }
audioManager.onDeviceUpdate?(audioManager)
}

// Engine events

func audioDeviceModule(_: LKRTCAudioDeviceModule, didCreateEngine engine: AVAudioEngine) {
guard let audioManager else { return }
let entryPoint = audioManager._state.engineObservers.buildChain()
entryPoint?.engineDidCreate(engine)
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, willEnableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) {
guard let audioManager else { return }
let entryPoint = audioManager._state.engineObservers.buildChain()
entryPoint?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled)
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, willStartEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) {
guard let audioManager else { return }
let entryPoint = audioManager._state.engineObservers.buildChain()
entryPoint?.engineWillStart(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled)
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, didStopEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) {
guard let audioManager else { return }
let entryPoint = audioManager._state.engineObservers.buildChain()
entryPoint?.engineDidStop(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled)
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, didDisableEngine engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) {
guard let audioManager else { return }
let entryPoint = audioManager._state.engineObservers.buildChain()
entryPoint?.engineDidDisable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled)
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, willReleaseEngine engine: AVAudioEngine) {
guard let audioManager else { return }
let entryPoint = audioManager._state.engineObservers.buildChain()
entryPoint?.engineWillRelease(engine)
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, engine: AVAudioEngine, configureInputFromSource src: AVAudioNode?, toDestination dst: AVAudioNode, format: AVAudioFormat) -> Bool {
guard let audioManager else { return false }
let entryPoint = audioManager._state.engineObservers.buildChain()
return entryPoint?.engineWillConnectInput(engine, src: src, dst: dst, format: format) ?? false
}

func audioDeviceModule(_: LKRTCAudioDeviceModule, engine: AVAudioEngine, configureOutputFromSource src: AVAudioNode, toDestination dst: AVAudioNode?, format: AVAudioFormat) -> Bool {
guard let audioManager else { return false }
let entryPoint = audioManager._state.engineObservers.buildChain()
return entryPoint?.engineWillConnectOutput(engine, src: src, dst: dst, format: format) ?? false
}
}
63 changes: 63 additions & 0 deletions Sources/LiveKit/Audio/AudioEngineObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import AVFAudio

/// Do not retain the engine object.
public protocol AudioEngineObserver: NextInvokable, Sendable {
func setNext(_ handler: any AudioEngineObserver)

func engineDidCreate(_ engine: AVAudioEngine)
func engineWillEnable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
func engineWillStart(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
func engineDidStop(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
func engineDidDisable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool)
func engineWillRelease(_ engine: AVAudioEngine)

/// Provide custom implementation for internal AVAudioEngine's output configuration.
/// Buffers flow from `src` to `dst`. Preferred format to connect node is provided as `format`.
/// Return true if custom implementation is provided, otherwise default implementation will be used.
func engineWillConnectOutput(_ engine: AVAudioEngine, src: AVAudioNode, dst: AVAudioNode?, format: AVAudioFormat) -> Bool
/// Provide custom implementation for internal AVAudioEngine's input configuration.
/// Buffers flow from `src` to `dst`. Preferred format to connect node is provided as `format`.
/// Return true if custom implementation is provided, otherwise default implementation will be used.
func engineWillConnectInput(_ engine: AVAudioEngine, src: AVAudioNode?, dst: AVAudioNode, format: AVAudioFormat) -> Bool
}

/// Default implementation to make it optional.
public extension AudioEngineObserver {
func engineDidCreate(_: AVAudioEngine) {}
func engineWillEnable(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {}
func engineWillStart(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {}
func engineDidStop(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {}
func engineDidDisable(_: AVAudioEngine, isPlayoutEnabled _: Bool, isRecordingEnabled _: Bool) {}
func engineWillRelease(_: AVAudioEngine) {}

func engineWillConnectOutput(_: AVAudioEngine, src _: AVAudioNode, dst _: AVAudioNode?, format _: AVAudioFormat) -> Bool { false }
func engineWillConnectInput(_: AVAudioEngine, src _: AVAudioNode?, dst _: AVAudioNode, format _: AVAudioFormat) -> Bool { false }
}

extension [any AudioEngineObserver] {
func buildChain() -> Element? {
guard let first else { return nil }

for i in 0 ..< count - 1 {
self[i].setNext(self[i + 1])
}

return first
}
}
126 changes: 126 additions & 0 deletions Sources/LiveKit/Audio/DefaultAudioSessionObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if os(iOS) || os(visionOS) || os(tvOS)

import AVFoundation

#if swift(>=5.9)
internal import LiveKitWebRTC
#else
@_implementationOnly import LiveKitWebRTC
#endif

public final class DefaultAudioSessionObserver: AudioEngineObserver, Loggable {
struct State {
var isSessionActive = false
var next: (any AudioEngineObserver)?

// Used for backward compatibility with `customConfigureAudioSessionFunc`.
var isPlayoutEnabled: Bool = false
var isRecordingEnabled: Bool = false
}

let _state = StateSync(State())

init() {
// Backward compatibility with `customConfigureAudioSessionFunc`.
_state.onDidMutate = { new_, old_ in
if let config_func = AudioManager.shared._state.customConfigureFunc,
new_.isPlayoutEnabled != old_.isPlayoutEnabled ||
new_.isRecordingEnabled != old_.isRecordingEnabled
{
// Simulate state and invoke custom config func.
let old_state = AudioManager.State(localTracksCount: old_.isRecordingEnabled ? 1 : 0, remoteTracksCount: old_.isPlayoutEnabled ? 1 : 0)
let new_state = AudioManager.State(localTracksCount: new_.isRecordingEnabled ? 1 : 0, remoteTracksCount: new_.isPlayoutEnabled ? 1 : 0)
config_func(new_state, old_state)
}
}
}

public func setNext(_ nextHandler: any AudioEngineObserver) {
_state.mutate { $0.next = nextHandler }
}

public func engineWillEnable(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) {
if AudioManager.shared._state.customConfigureFunc == nil {
log("Configuring audio session...")
let session = LKRTCAudioSession.sharedInstance()
session.lockForConfiguration()
defer { session.unlockForConfiguration() }

let config: AudioSessionConfiguration = isRecordingEnabled ? .playAndRecordSpeaker : .playback
do {
if _state.isSessionActive {
log("AudioSession switching category to: \(config.category)")
try session.setConfiguration(config.toRTCType())
} else {
log("AudioSession activating category to: \(config.category)")
try session.setConfiguration(config.toRTCType(), active: true)
_state.mutate { $0.isSessionActive = true }
}
} catch {
log("AudioSession failed to configure with error: \(error)", .error)
}

log("AudioSession activationCount: \(session.activationCount), webRTCSessionCount: \(session.webRTCSessionCount)")
}

_state.mutate {
$0.isPlayoutEnabled = isPlayoutEnabled
$0.isRecordingEnabled = isRecordingEnabled
}

// Call next last
_state.next?.engineWillEnable(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled)
}

public func engineDidStop(_ engine: AVAudioEngine, isPlayoutEnabled: Bool, isRecordingEnabled: Bool) {
// Call next first
_state.next?.engineDidStop(engine, isPlayoutEnabled: isPlayoutEnabled, isRecordingEnabled: isRecordingEnabled)

_state.mutate {
$0.isPlayoutEnabled = isPlayoutEnabled
$0.isRecordingEnabled = isRecordingEnabled
}

if AudioManager.shared._state.customConfigureFunc == nil {
log("Configuring audio session...")
let session = LKRTCAudioSession.sharedInstance()
session.lockForConfiguration()
defer { session.unlockForConfiguration() }

do {
if isPlayoutEnabled, !isRecordingEnabled {
let config: AudioSessionConfiguration = .playback
log("AudioSession switching category to: \(config.category)")
try session.setConfiguration(config.toRTCType())
}
if !isPlayoutEnabled, !isRecordingEnabled {
log("AudioSession deactivating")
try session.setActive(false)
_state.mutate { $0.isSessionActive = false }
}
} catch {
log("AudioSession failed to configure with error: \(error)", .error)
}

log("AudioSession activationCount: \(session.activationCount), webRTCSessionCount: \(session.webRTCSessionCount)")
}
}
}

#endif
16 changes: 1 addition & 15 deletions Sources/LiveKit/Core/RTC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,6 @@ private class VideoEncoderFactorySimulcast: LKRTCVideoEncoderFactorySimulcast {
}

class RTC {
private static var _bypassVoiceProcessing: Bool = false
private static var _peerConnectionFactoryInitialized = false

static var bypassVoiceProcessing: Bool {
get { _bypassVoiceProcessing }
set {
if _peerConnectionFactoryInitialized {
logger.log("Warning: Setting bypassVoiceProcessing after PeerConnectionFactory initialization has no effect. Set it at application launch.", .warning, type: Room.self)
}
_bypassVoiceProcessing = newValue
}
}

static let h264BaselineLevel5CodecInfo: LKRTCVideoCodecInfo = {
// this should never happen
guard let profileLevelId = LKRTCH264ProfileLevelId(profile: .constrainedBaseline, level: .level5) else {
Expand Down Expand Up @@ -100,8 +87,7 @@ class RTC {

logger.log("Initializing PeerConnectionFactory...", type: Room.self)

_peerConnectionFactoryInitialized = true
return LKRTCPeerConnectionFactory(bypassVoiceProcessing: bypassVoiceProcessing,
return LKRTCPeerConnectionFactory(bypassVoiceProcessing: false,
encoderFactory: encoderFactory,
decoderFactory: decoderFactory,
audioProcessingModule: audioProcessingModule)
Expand Down
7 changes: 3 additions & 4 deletions Sources/LiveKit/Core/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -518,12 +518,11 @@ extension Room: AppStateDelegate {

public extension Room {
/// Set this to true to bypass initialization of voice processing.
/// Must be set before RTCPeerConnectionFactory gets initialized.
/// The most reliable place to set this is in your application's initialization process.
@available(*, deprecated, renamed: "AudioManager.shared.isVoiceProcessingBypassed")
@objc
static var bypassVoiceProcessing: Bool {
get { RTC.bypassVoiceProcessing }
set { RTC.bypassVoiceProcessing = newValue }
get { AudioManager.shared.isVoiceProcessingBypassed }
set { AudioManager.shared.isVoiceProcessingBypassed = newValue }
}
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/LiveKit/Extensions/CustomStringConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,14 @@ extension AVCaptureDevice.Format {
return "Format(\(values.joined(separator: ", ")))"
}
}

extension LKRTCAudioProcessingConfig {
func toDebugString() -> String {
"RTCAudioProcessingConfig(" +
"isEchoCancellationEnabled: \(isEchoCancellationEnabled), " +
"isNoiseSuppressionEnabled: \(isNoiseSuppressionEnabled), " +
"isAutoGainControl1Enabled: \(isAutoGainControl1Enabled), " +
"isHighpassFilterEnabled: \(isHighpassFilterEnabled)" +
")"
}
}
22 changes: 22 additions & 0 deletions Sources/LiveKit/Protocols/NextInvokable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

public protocol NextInvokable {
associatedtype Next
func setNext(_ handler: Next)
}
2 changes: 1 addition & 1 deletion Sources/LiveKit/Support/StateSync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Combine
import Foundation

@dynamicMemberLookup
public final class StateSync<State> {
public final class StateSync<State>: @unchecked Sendable {
// MARK: - Types

public typealias OnDidMutate = (_ newState: State, _ oldState: State) -> Void
Expand Down
Loading
Loading