diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index 67a1a2d..5285cdb 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin.git", "state" : { - "revision" : "d1691545d53581400b1de9b0472d45eb25c19fed", - "version" : "1.4.4" + "revision" : "647c708be89f834fa6a6d4945442793a77ddf5b6", + "version" : "1.5.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "2547102afd04fe49f1b286090f13ebce07284980", - "version" : "1.31.1" + "revision" : "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8", + "version" : "1.38.1" } }, { diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index 2c86ebe..7380983 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -648,7 +648,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 20260609; + CURRENT_PROJECT_VERSION = 20260717.1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 76TVFCUKK7; @@ -670,7 +670,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MARKETING_VERSION = 2.15.0; + MARKETING_VERSION = 2.15.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -717,7 +717,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 20260609; + CURRENT_PROJECT_VERSION = 20260717.1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 76TVFCUKK7; @@ -734,7 +734,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MARKETING_VERSION = 2.15.0; + MARKETING_VERSION = 2.15.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/Multiplatform/Controllers/AppContext.swift b/Multiplatform/Controllers/AppContext.swift index 62bb471..4d61bf3 100644 --- a/Multiplatform/Controllers/AppContext.swift +++ b/Multiplatform/Controllers/AppContext.swift @@ -90,17 +90,67 @@ final class AppContext: NSObject, ObservableObject { didSet { AudioManager.shared.isVoiceProcessingAGCEnabled = isVoiceProcessingAGCEnabled } } - @Published var isVoiceProcessingEnabled: Bool = true { + @Published var isPlatformVoiceProcessingAllowed: Bool = true { didSet { - guard oldValue != isVoiceProcessingEnabled else { return } + guard oldValue != isPlatformVoiceProcessingAllowed else { return } do { - try AudioManager.shared.setVoiceProcessingEnabled(isVoiceProcessingEnabled) + try AudioManager.shared.setPlatformVoiceProcessingAllowed(isPlatformVoiceProcessingAllowed) } catch { - print("Failed to set voice processing enabled: \(error)") + print("Failed to set platform voice processing allowed: \(error)") } } } + @Published var runtimeEchoCancellation: Bool = true + @Published var runtimeNoiseSuppression: Bool = true + @Published var runtimeAutoGainControl: Bool = true + @Published var runtimeHighPassFilter: Bool = false + @Published var runtimeEchoCancellationMode: EchoCancellationMode = .automatic + @Published var runtimeNoiseSuppressionMode: NoiseSuppressionMode = .automatic + @Published var runtimeAutoGainControlMode: AutoGainControlMode = .automatic + @Published var runtimeHighPassFilterMode: HighpassFilterMode = .automatic + @Published var runtimeAudioProcessingStatus: String = "" + @Published var audioProcessingSummary: String = "" + @Published var runtimeAudioProcessingEffectiveStates: [AudioProcessingEffectiveState] = [] + + var runtimeAudioProcessingOptions: AudioProcessingOptions { + AudioProcessingOptions( + echoCancellation: runtimeEchoCancellation, + autoGainControl: runtimeAutoGainControl, + noiseSuppression: runtimeNoiseSuppression, + highpassFilter: runtimeHighPassFilter, + echoCancellationMode: runtimeEchoCancellationMode, + autoGainControlMode: runtimeAutoGainControlMode, + noiseSuppressionMode: runtimeNoiseSuppressionMode, + highpassFilterMode: runtimeHighPassFilterMode + ) + } + + var runtimeAudioCaptureOptions: AudioCaptureOptions { + let options = runtimeAudioProcessingOptions + return AudioCaptureOptions( + echoCancellation: options.echoCancellation, + autoGainControl: options.autoGainControl, + noiseSuppression: options.noiseSuppression, + highpassFilter: options.highpassFilter, + echoCancellationMode: options.echoCancellationMode, + autoGainControlMode: options.autoGainControlMode, + noiseSuppressionMode: options.noiseSuppressionMode, + highpassFilterMode: options.highpassFilterMode + ) + } + + func setRuntimeProcessingControls(_ options: AudioProcessingOptions) { + runtimeEchoCancellation = options.echoCancellation + runtimeAutoGainControl = options.autoGainControl + runtimeNoiseSuppression = options.noiseSuppression + runtimeHighPassFilter = options.highpassFilter + runtimeEchoCancellationMode = options.echoCancellationMode + runtimeAutoGainControlMode = options.autoGainControlMode + runtimeNoiseSuppressionMode = options.noiseSuppressionMode + runtimeHighPassFilterMode = options.highpassFilterMode + } + @Published var micMuteMode: MicrophoneMuteMode = .voiceProcessing { didSet { do { @@ -127,7 +177,10 @@ final class AppContext: NSObject, ObservableObject { didSet { Task { do { - try await AudioManager.shared.setRecordingAlwaysPreparedMode(isRecordingAlwaysPreparedMode) + try await AudioManager.shared.setRecordingAlwaysPreparedMode( + isRecordingAlwaysPreparedMode, + audioProcessingOptions: runtimeAudioProcessingOptions + ) } catch { print("Failed to set recording always prepared mode: \(error)") } @@ -223,14 +276,22 @@ final class AppContext: NSObject, ObservableObject { inputDevices = AudioManager.shared.inputDevices outputDevice = AudioManager.shared.outputDevice inputDevice = AudioManager.shared.inputDevice - isVoiceProcessingEnabled = AudioManager.shared.isVoiceProcessingEnabled + isPlatformVoiceProcessingAllowed = AudioManager.shared.isPlatformVoiceProcessingAllowed isVoiceProcessingAGCEnabled = AudioManager.shared.isVoiceProcessingAGCEnabled isRecordingAlwaysPreparedMode = AudioManager.shared.isRecordingAlwaysPreparedMode + refreshAudioProcessingState() updateAudioDeviceSelections() } } -private extension AppContext { +struct AudioProcessingEffectiveState: Identifiable, Sendable { + let id: String + let title: String + let result: AudioProcessingImplementation + let detail: String +} + +extension AppContext { func updateAudioDeviceSelections() { if !inputDevices.contains(where: { $0.id == inputDevice.id }) { if let defaultInput = inputDevices.first(where: { $0.isDefault }) { @@ -248,6 +309,145 @@ private extension AppContext { } } } + + func refreshAudioProcessingState() { + let state = AudioManager.shared.audioProcessingState + let platformState = AudioManager.shared.platformVoiceProcessingState + let engineAvailability = AudioManager.shared.engineAvailability + let topology = switch platformState.topology { + case .independent: "independent" + case .echoCancellationAndNoiseSuppressionCoupled: "AEC/NS coupled" + } + let audioProcessingOptions = runtimeAudioProcessingOptions + runtimeAudioProcessingEffectiveStates = audioProcessingEffectiveStates(for: state) + audioProcessingSummary = [ + "LiveKit audio processing diagnostics", + "generatedAt: \(ISO8601DateFormatter().string(from: Date()))", + "platform: \(platformName)", + "", + "App voice processing controls", + " platformVoiceProcessingAllowed: \(boolSummary(AudioManager.shared.isPlatformVoiceProcessingAllowed))", + " voiceProcessingBypassed: \(boolSummary(AudioManager.shared.isVoiceProcessingBypassed))", + " voiceProcessingAGCEnabled: \(boolSummary(AudioManager.shared.isVoiceProcessingAGCEnabled))", + "", + "Runtime AudioProcessingOptions controls", + " echoCancellation: \(componentRequest(audioProcessingOptions.echoCancellation, audioProcessingOptions.echoCancellationMode))", + " noiseSuppression: \(componentRequest(audioProcessingOptions.noiseSuppression, audioProcessingOptions.noiseSuppressionMode))", + " autoGainControl: \(componentRequest(audioProcessingOptions.autoGainControl, audioProcessingOptions.autoGainControlMode))", + " highPassFilter: \(componentRequest(audioProcessingOptions.highpassFilter, audioProcessingOptions.highpassFilterMode))", + "", + "Current effective processing", + " echoCancellation: \(effectiveStateSummary(state.echoCancellation))", + " noiseSuppression: \(effectiveStateSummary(state.noiseSuppression))", + " autoGainControl: \(effectiveStateSummary(state.autoGainControl))", + " highPassFilter: \(effectiveStateSummary(state.highpassFilter))", + "", + "Engine audio processing state", + " echoCancellation: \(runtimeComponentSummary(state.echoCancellation))", + " noiseSuppression: \(runtimeComponentSummary(state.noiseSuppression))", + " autoGainControl: \(runtimeComponentSummary(state.autoGainControl))", + " highPassFilter: \(runtimeComponentSummary(state.highpassFilter))", + "", + "Audio engine", + " engineRunning: \(boolSummary(AudioManager.shared.isEngineRunning))", + " inputAvailable requested: \(boolSummary(isAudioEngineInputAvailable))", + " inputAvailable effective: \(boolSummary(engineAvailability.isInputAvailable))", + " outputAvailable requested: \(boolSummary(isAudioEngineOutputAvailable))", + " outputAvailable effective: \(boolSummary(engineAvailability.isOutputAvailable))", + "", + "Platform audio processing topology", + " topology: \(topology)", + " echoCancellation: \(componentSummary(platformState.echoCancellation))", + " noiseSuppression: \(componentSummary(platformState.noiseSuppression))", + " autoGainControl: \(componentSummary(platformState.autoGainControl))", + "", + "Apple Voice Processing I/O state", + " voiceProcessingEnabled requested: \(boolSummary(platformState.voiceProcessingEnabled.isRequested))", + " voiceProcessingEnabled active: \(boolSummary(platformState.voiceProcessingEnabled.isActive))", + " voiceProcessingBypassed requested: \(boolSummary(platformState.voiceProcessingBypassed.isRequested))", + " voiceProcessingBypassed active: \(boolSummary(platformState.voiceProcessingBypassed.isActive))", + " voiceProcessingAGC requested: \(boolSummary(platformState.voiceProcessingAGCEnabled.isRequested))", + " voiceProcessingAGC active: \(boolSummary(platformState.voiceProcessingAGCEnabled.isActive))", + "", + "Notes", + " engine state comes from the factory-owned audio processing module.", + " requested values come from the ADM state.", + " active values come from the platform input node when available.", + " active values read off before the input path is configured.", + " subscribe-only playback does not configure the input path.", + ].joined(separator: "\n") + } + + func audioProcessingEffectiveStates(for state: AudioProcessingState) -> [AudioProcessingEffectiveState] { + [ + audioProcessingEffectiveState(id: "aec", title: "AEC", component: state.echoCancellation), + audioProcessingEffectiveState(id: "ns", title: "NS", component: state.noiseSuppression), + audioProcessingEffectiveState(id: "agc", title: "AGC", component: state.autoGainControl), + audioProcessingEffectiveState(id: "hpf", title: "HPF", component: state.highpassFilter), + ] + } + + func audioProcessingEffectiveState( + id: String, + title: String, + component: AudioProcessingComponentState + ) -> AudioProcessingEffectiveState { + AudioProcessingEffectiveState( + id: id, + title: title, + result: component.effective, + detail: runtimeComponentDetail(component) + ) + } + + func componentSummary(_ state: PlatformVoiceProcessingComponentState) -> String { + "available: \(boolSummary(state.isAvailable)), " + + "requested: \(boolSummary(state.isRequested)), " + + "active: \(boolSummary(state.isActive))" + } + + func effectiveStateSummary(_ component: AudioProcessingComponentState) -> String { + "result: \(component.effective.description), \(runtimeComponentDetail(component))" + } + + func runtimeComponentSummary(_ component: AudioProcessingComponentState) -> String { + let requested = component.requested + .map { "\(boolSummary($0.isEnabled)) / \($0.mode)" } ?? "none" + let platform = component.platform + .map { "available: On, resolved: \(boolSummary($0.isResolved)), active: \(boolSummary($0.isActive))" } + ?? "available: Off" + return "effective: \(component.effective.description), " + + "requested: \(requested), " + + "softwareResolved: \(boolSummary(component.software.isResolved)), " + + "softwareActive: \(boolSummary(component.software.isActive)), " + + "platform: \(platform)" + } + + func runtimeComponentDetail(_ component: AudioProcessingComponentState) -> String { + "Software: \(boolSummary(component.software.isActive)), Platform: \(boolSummary(component.platform?.isActive ?? false))" + } + + func boolSummary(_ value: Bool) -> String { + value ? "On" : "Off" + } + + func componentRequest(_ enabled: Bool, _ mode: Mode) -> String { + "enabled: \(boolSummary(enabled)), mode: \(mode)" + } + + var platformName: String { + #if os(iOS) + "iOS" + #elseif os(macOS) + "macOS" + #elseif os(visionOS) + "visionOS" + #elseif os(tvOS) + "tvOS" + #else + "unknown" + #endif + } } // MARK: - AudioClips diff --git a/Multiplatform/Controllers/RoomContext.swift b/Multiplatform/Controllers/RoomContext.swift index a237192..b02a6cc 100644 --- a/Multiplatform/Controllers/RoomContext.swift +++ b/Multiplatform/Controllers/RoomContext.swift @@ -144,7 +144,9 @@ final class RoomContext: ObservableObject { _connectTask?.cancel() } - func connect(entry: ConnectionHistory? = nil) async throws -> Room { + func connect(entry: ConnectionHistory? = nil, + audioCaptureOptions: AudioCaptureOptions? = nil) async throws -> Room + { if let entry { url = entry.url token = entry.token @@ -172,6 +174,7 @@ final class RoomContext: ObservableObject { appAudio: true, useBroadcastExtension: true ), + defaultAudioCaptureOptions: audioCaptureOptions ?? AudioCaptureOptions(), defaultVideoPublishOptions: VideoPublishOptions( simulcast: simulcast ), diff --git a/Multiplatform/Views/AudioControlsPanel.swift b/Multiplatform/Views/AudioControlsPanel.swift index d69d1f1..f9acad7 100644 --- a/Multiplatform/Views/AudioControlsPanel.swift +++ b/Multiplatform/Views/AudioControlsPanel.swift @@ -17,9 +17,16 @@ import LiveKit import SwiftUI +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + #if !os(tvOS) struct AudioControlsPanel: View { @EnvironmentObject var appCtx: AppContext + @EnvironmentObject var room: Room private var inputDeviceSelection: Binding { Binding( @@ -83,11 +90,75 @@ struct AudioControlsPanel: View { } Section(header: Text("Voice Processing")) { - Toggle("Voice processing enabled", isOn: $appCtx.isVoiceProcessingEnabled) + Toggle("Platform voice processing allowed", isOn: $appCtx.isPlatformVoiceProcessingAllowed) Toggle("Bypass voice processing", isOn: $appCtx.isVoiceProcessingBypassed) Toggle("Auto gain control (AGC)", isOn: $appCtx.isVoiceProcessingAGCEnabled) } + Section(header: Text("Runtime Audio Processing")) { + processingRow("Echo cancellation", + isOn: $appCtx.runtimeEchoCancellation, + mode: $appCtx.runtimeEchoCancellationMode) + + processingRow("Noise suppression", + isOn: $appCtx.runtimeNoiseSuppression, + mode: $appCtx.runtimeNoiseSuppressionMode) + + processingRow("Auto gain control", + isOn: $appCtx.runtimeAutoGainControl, + mode: $appCtx.runtimeAutoGainControlMode) + + processingRow("High-pass filter", + isOn: $appCtx.runtimeHighPassFilter, + mode: $appCtx.runtimeHighPassFilterMode) + + HStack { + Text("Presets") + .foregroundColor(.secondary) + Button("Default") { + appCtx.setRuntimeProcessingControls(AudioProcessingOptions()) + } + Button("No processing") { + appCtx.setRuntimeProcessingControls(.noProcessing) + } + } + .buttonStyle(.bordered) + + HStack { + Button("Apply to local mic") { + applyRuntimeAudioProcessingOptions() + } + Button("Get diagnostics") { + appCtx.refreshAudioProcessingState() + } + Button("Copy diagnostics") { + copyAudioProcessingDiagnostics() + } + } + .buttonStyle(.bordered) + + if !appCtx.runtimeAudioProcessingStatus.isEmpty { + Text(appCtx.runtimeAudioProcessingStatus) + .font(.caption) + .foregroundColor(.secondary) + } + + if !appCtx.runtimeAudioProcessingEffectiveStates.isEmpty { + AudioProcessingEffectiveStateBox(states: appCtx.runtimeAudioProcessingEffectiveStates) + } + + if !appCtx.audioProcessingSummary.isEmpty { + ScrollView { + Text(appCtx.audioProcessingSummary) + .font(.caption2.monospaced()) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + } + .frame(minHeight: 180, maxHeight: 260) + } + } + Section(header: Text("Recording")) { Toggle("Always prepared", isOn: $appCtx.isRecordingAlwaysPreparedMode) Text("Keeps mic pipeline warmed for low-latency publish.") @@ -177,7 +248,140 @@ struct AudioControlsPanel: View { } } +private struct AudioProcessingEffectiveStateBox: View { + let states: [AudioProcessingEffectiveState] + + private var columns: [GridItem] { + [GridItem(.adaptive(minimum: 120), spacing: 8, alignment: .top)] + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Current effective state") + .font(.caption.weight(.semibold)) + .foregroundColor(.secondary) + + LazyVGrid(columns: columns, alignment: .leading, spacing: 8) { + ForEach(states) { state in + AudioProcessingEffectiveStateItem(state: state) + } + } + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondary.opacity(0.08)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.18)) + ) + } +} + +private struct AudioProcessingEffectiveStateItem: View { + let state: AudioProcessingEffectiveState + + var body: some View { + HStack(alignment: .top, spacing: 8) { + Circle() + .fill(state.result.tintColor) + .frame(width: 10, height: 10) + .padding(.top, 4) + + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 4) { + Text(state.title) + .font(.caption.weight(.semibold)) + Text(state.result.description) + .font(.caption) + .foregroundColor(.primary) + } + Text(state.detail) + .font(.caption2) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +private extension AudioProcessingImplementation { + var tintColor: Color { + switch self { + case .platform, .software, .softwareAndPlatform: + return .green + case .disabled: + return .gray + case .unknown: + return .secondary + } + } +} + private extension AudioControlsPanel { + var localMicrophoneTrack: LocalAudioTrack? { + room.localParticipant.audioTracks + .first(where: { $0.source == .microphone })? + .track as? LocalAudioTrack + } + + func processingRow( + _ title: String, + isOn: Binding, + mode: Binding + ) -> some View where Mode.AllCases: RandomAccessCollection { + HStack(spacing: 12) { + Toggle(title, isOn: isOn) + .lineLimit(1) + + Spacer(minLength: 8) + + Picker("Mode", selection: mode) { + ForEach(Mode.allCases, id: \.self) { mode in + Text(String(describing: mode).capitalized).tag(mode) + } + } + .labelsHidden() + .pickerStyle(.menu) + .disabled(!isOn.wrappedValue) + .frame(minWidth: 110, maxWidth: 150, alignment: .trailing) + } + } + + func applyRuntimeAudioProcessingOptions() { + guard let localMicrophoneTrack else { + appCtx.runtimeAudioProcessingStatus = "Audio processing options: stored — applies when the mic is published" + appCtx.refreshAudioProcessingState() + return + } + + do { + let result = try localMicrophoneTrack.setAudioProcessingOptions(appCtx.runtimeAudioProcessingOptions) + appCtx.runtimeAudioProcessingStatus = switch result { + case .applied: "Audio processing options: applied" + case .stored: "Audio processing options: stored — applies when the mic is sending" + } + } catch { + appCtx.runtimeAudioProcessingStatus = "Failed: \(error)" + } + appCtx.refreshAudioProcessingState() + } + + func copyAudioProcessingDiagnostics() { + appCtx.refreshAudioProcessingState() + let diagnostics = appCtx.audioProcessingSummary + #if canImport(UIKit) + UIPasteboard.general.string = diagnostics + #elseif canImport(AppKit) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(diagnostics, forType: .string) + #endif + appCtx.runtimeAudioProcessingStatus = "Diagnostics copied." + } + func micMuteModeDescription(for mode: MicrophoneMuteMode) -> String { switch mode { case .voiceProcessing: diff --git a/Multiplatform/Views/ConnectView.swift b/Multiplatform/Views/ConnectView.swift index f27e612..b4cb011 100644 --- a/Multiplatform/Views/ConnectView.swift +++ b/Multiplatform/Views/ConnectView.swift @@ -98,7 +98,7 @@ struct ConnectView: View { LKButton(title: "Connect") { Task { @MainActor in do { - let room = try await roomCtx.connect() + let room = try await roomCtx.connect(audioCaptureOptions: appCtx.runtimeAudioCaptureOptions) appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) } catch { print("Failed to connect: \(error)") @@ -112,7 +112,8 @@ struct ConnectView: View { Button { Task { @MainActor in do { - let room = try await roomCtx.connect(entry: entry) + let room = try await roomCtx.connect(entry: entry, + audioCaptureOptions: appCtx.runtimeAudioCaptureOptions) appCtx.connectionHistory.update(room: room, e2ee: roomCtx.isE2eeEnabled, e2eeKey: roomCtx.e2eeKey) } catch { print("Failed to connect: \(error)") diff --git a/Multiplatform/Views/RoomContextView.swift b/Multiplatform/Views/RoomContextView.swift index 906751f..e9fd47a 100644 --- a/Multiplatform/Views/RoomContextView.swift +++ b/Multiplatform/Views/RoomContextView.swift @@ -60,7 +60,7 @@ struct RoomContextView: View { roomCtx.e2eeKey = e2eeKey if !roomCtx.token.isEmpty { do { - let room = try await roomCtx.connect() + let room = try await roomCtx.connect(audioCaptureOptions: appCtx.runtimeAudioCaptureOptions) appCtx.connectionHistory.update(room: room, e2ee: e2ee, e2eeKey: e2eeKey) } catch { print("Failed to connect: \(error)") diff --git a/Multiplatform/Views/RoomView.swift b/Multiplatform/Views/RoomView.swift index 5f7ffea..8b87572 100644 --- a/Multiplatform/Views/RoomView.swift +++ b/Multiplatform/Views/RoomView.swift @@ -359,7 +359,9 @@ struct RoomView: View { Task { isMicrophonePublishingBusy = true defer { Task { @MainActor in isMicrophonePublishingBusy = false } } - _ = try? await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled) + // Use the audio processing options stored in the controls panel when publishing + _ = try? await room.localParticipant.setMicrophone(enabled: !isMicrophoneEnabled, + captureOptions: isMicrophoneEnabled ? nil : appCtx.runtimeAudioCaptureOptions) } }, label: {