AirPods手势无法触发AVAudioApplication输入静音状态通知的问询
问题
我开发了一款获取用户麦克风信息的macOS Swift应用,尝试使用macOS 14+的AVAudioApplication新API,让用户通过AirPods压杆手势实现音频输入静音。根据WWDC文档,有两种实现方式:在应用层接收通知处理,或使用底层CoreAudio API,我选择前者。
运行代码需满足两个前置条件:
- 在应用能力中添加
Audio Input权限 - 在
Info.plist中配置NSMicrophoneUsageDescription键
核心逻辑代码如下:
class Manager: ObservableObject { private var controller: AudioInputController? private var cancellable: AnyCancellable? init() { cancellable = NotificationCenter.default.publisher(for: AVAudioApplication.inputMuteStateChangeNotification) .sink { notification in print("Notification", notification) } do { try AVAudioApplication.shared.setInputMuteStateChangeHandler { isMuted in print("Mute state", isMuted, Date()) return true } } catch { assertionFailure() print("Error setting up handler", error) } controller = AudioInputController()! controller?.start() } }
完整实现代码:
class Manager: ObservableObject { private var controller: AudioInputController? private var cancellable: AnyCancellable? init() { cancellable = NotificationCenter.default.publisher(for: AVAudioApplication.inputMuteStateChangeNotification) .sink { notification in print("Notification", notification) } do { try AVAudioApplication.shared.setInputMuteStateChangeHandler { isMuted in print("Mute state", isMuted, Date()) return true } } catch { assertionFailure() print("Error setting up handler", error) } controller = AudioInputController()! controller?.start() } } struct ContentView: View { @StateObject private var manager = Manager() var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) } .padding() } } func getDefaultAudioDeviceID() -> AudioDeviceID? { var deviceID = AudioDeviceID() var dataSize = UInt32(MemoryLayout<AudioDeviceID>.size) var propertyAddress = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeInput, mElement: kAudioObjectPropertyElementMain ) let status = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &dataSize, &deviceID ) guard status == noErr else { assertionFailure() return nil } return deviceID } private final class AudioInputController { private var auHAL: AudioComponentInstance? private var inputBufferList: UnsafeMutableAudioBufferListPointer? private var sampleRate: Float = 0.0 init?() { guard let audioDeviceID = getDefaultAudioDeviceID() else { assertionFailure() return nil } var osStatus: OSStatus = noErr // Create an AUHAL instance. var description = AudioComponentDescription( componentType: kAudioUnitType_Output, componentSubType: kAudioUnitSubType_HALOutput, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0 ) guard let component = AudioComponentFindNext(nil, &description) else { assertionFailure() return } osStatus = AudioComponentInstanceNew(component, &auHAL) guard osStatus == noErr, let auHAL else { return nil } // Enable the input bus, and disable the output bus. let kInputElement: UInt32 = 1 let kOutputElement: UInt32 = 0 var kInputData: UInt32 = 1 var kOutputData: UInt32 = 0 let ioDataSize: UInt32 = UInt32(MemoryLayout<UInt32>.size) osStatus = AudioUnitSetProperty( auHAL, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputElement, &kInputData, ioDataSize ) guard osStatus == noErr else { assertionFailure() return nil } osStatus = AudioUnitSetProperty( auHAL, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputElement, &kOutputData, ioDataSize ) if osStatus != noErr { assertionFailure() } var inputDevice: AudioDeviceID = audioDeviceID let inputDeviceSize: UInt32 = UInt32(MemoryLayout<AudioDeviceID>.size) osStatus = AudioUnitSetProperty( auHAL, AudioUnitPropertyID(kAudioOutputUnitProperty_CurrentDevice), AudioUnitScope(kAudioUnitScope_Global), 0, &inputDevice, inputDeviceSize ) guard osStatus == noErr else { assertionFailure() return nil } // Adopt the stream format. var deviceFormat = AudioStreamBasicDescription() var desiredFormat = AudioStreamBasicDescription() var ioFormatSize: UInt32 = UInt32(MemoryLayout<AudioStreamBasicDescription>.size) osStatus = AudioUnitGetProperty( auHAL, AudioUnitPropertyID(kAudioUnitProperty_StreamFormat), AudioUnitScope(kAudioUnitScope_Input), kInputElement, &deviceFormat, &ioFormatSize ) guard osStatus == noErr else { assertionFailure() return nil } osStatus = AudioUnitGetProperty( auHAL, AudioUnitPropertyID(kAudioUnitProperty_StreamFormat), AudioUnitScope(kAudioUnitScope_Output), kInputElement, &desiredFormat, &ioFormatSize ) guard osStatus == noErr else { assertionFailure() return nil } // Same sample rate, same number of channels. desiredFormat.mSampleRate = deviceFormat.mSampleRate desiredFormat.mChannelsPerFrame = deviceFormat.mChannelsPerFrame // Canonical audio format. desiredFormat.mFormatID = kAudioFormatLinearPCM desiredFormat .mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved desiredFormat.mFramesPerPacket = 1 desiredFormat.mBytesPerFrame = UInt32(MemoryLayout<Float32>.size) desiredFormat.mBytesPerPacket = UInt32(MemoryLayout<Float32>.size) desiredFormat.mBitsPerChannel = 8 * UInt32(MemoryLayout<Float32>.size) osStatus = AudioUnitSetProperty( auHAL, AudioUnitPropertyID(kAudioUnitProperty_StreamFormat), AudioUnitScope(kAudioUnitScope_Output), kInputElement, &desiredFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size) ) guard osStatus == noErr else { assertionFailure() return nil } // Store the format information. sampleRate = Float(desiredFormat.mSampleRate) // Get the buffer frame size. var bufferSizeFrames: UInt32 = 0 var bufferSizeFramesSize = UInt32(MemoryLayout<UInt32>.size) osStatus = AudioUnitGetProperty( auHAL, AudioUnitPropertyID(kAudioDevicePropertyBufferFrameSize), AudioUnitScope(kAudioUnitScope_Global), 0, &bufferSizeFrames, &bufferSizeFramesSize ) guard osStatus == noErr else { assertionFailure() return nil } let bufferSizeBytes: UInt32 = bufferSizeFrames * UInt32(MemoryLayout<Float32>.size) let channels: UInt32 = deviceFormat.mChannelsPerFrame inputBufferList = AudioBufferList.allocate(maximumBuffers: Int(channels)) for i in 0 ..< Int(channels) { inputBufferList?[i] = AudioBuffer( mNumberChannels: channels, mDataByteSize: UInt32(bufferSizeBytes), mData: malloc(Int(bufferSizeBytes)) ) } var callbackStruct = AURenderCallbackStruct( inputProc: { ( inRefCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>, inTimeStamp: UnsafePointer<AudioTimeStamp>, inBusNumber: UInt32, inNumberFrame: UInt32, _: UnsafeMutablePointer<AudioBufferList>? ) -> OSStatus in let owner = Unmanaged<AudioInputController>.fromOpaque(inRefCon).takeUnretainedValue() owner.inputCallback( ioActionFlags: ioActionFlags, inTimeStamp: inTimeStamp, inBusNumber: inBusNumber, inNumberFrame: inNumberFrame ) return noErr }, inputProcRefCon: Unmanaged.passUnretained(self).toOpaque() ) osStatus = AudioUnitSetProperty( auHAL, AudioUnitPropertyID(kAudioOutputUnitProperty_SetInputCallback), AudioUnitScope(kAudioUnitScope_Global), 0, &callbackStruct, UInt32(MemoryLayout<AURenderCallbackStruct>.size) ) guard osStatus == noErr else { assertionFailure() return nil } osStatus = AudioUnitInitialize(auHAL) guard osStatus == noErr else { assertionFailure() return nil } } deinit { if let auHAL { AudioOutputUnitStop(auHAL) AudioComponentInstanceDispose(auHAL) } if let inputBufferList { for buffer in inputBufferList { free(buffer.mData) } } } private func inputCallback( ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>, inTimeStamp: UnsafePointer<AudioTimeStamp>, inBusNumber: UInt32, inNumberFrame: UInt32 ) { guard let inputBufferList, let auHAL else { assertionFailure() return } let err = AudioUnitRender( auHAL, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrame, inputBufferList.unsafeMutablePointer ) guard err == noErr else { assertionFailure() return } } func start() { guard let auHAL else { assertionFailure() return } let status: OSStatus = AudioOutputUnitStart(auHAL) if status != noErr { assertionFailure() } } func stop() { guard let auHAL else { assertionFailure() return } let status: OSStatus = AudioOutputUnitStop(auHAL) if status != noErr {} } }
当前问题:按压第二代AirPods Pro的压杆时,仅出现系统静音提示,但AVAudioApplication.inputMuteStateChangeNotification通知或AVAudioApplication.shared.setInputMuteStateChangeHandler回调均未触发。需要确保按压压杆时这两个回调能正常调用。
解决方案
1. 激活AVAudioSession并确认权限
AVAudioApplication的回调依赖于应用处于活跃的音频输入会话状态。在Manager的init方法中添加音频会话激活逻辑:
do { let session = AVAudioSession.sharedInstance() // 仅需要输入时可使用.record类别 try session.setCategory(.playAndRecord, mode: .default) try session.setActive(true) // 请求麦克风权限(如果尚未授权) session.requestRecordPermission { granted in if !granted { print("麦克风权限未授予") } } } catch { print("激活音频会话失败:", error) }
2. 确保应用是活跃的音频输入客户端
系统仅会将AirPods静音事件发送给当前正在捕获音频数据的应用。在AudioInputController的inputCallback中添加简单的数据读取操作,避免会话被系统挂起:
private func inputCallback(...) { // 现有代码... // 读取一个样本数据,保持会话活跃 if let buffer = inputBufferList?.first?.mData { _ = buffer.load(as: Float32.self) } }
3. 验证AirPods手势配置
打开系统设置→蓝牙→点击AirPods旁的i图标,确认压杆手势设置为**"静音/取消静音"**,而非其他功能(如播放/暂停)。
4. 检查macOS版本兼容性
确保测试设备运行macOS 14.0或更高版本,AVAudioApplication的输入静音回调是该版本新增的API。
5. 确认应用权限配置
- 检查应用的
Signing & Capabilities中已添加Audio Input权限 - 前往系统设置→隐私与安全性→麦克风,确认应用已被授予麦克风访问权限
内容的提问来源于stack exchange,提问作者jnpdx
相关产品推荐
相关产品推荐

