我有一个macOS Swift应用程序,可以从用户麦克风获取信息.我正在try 使用AVAudioApplication
上提供的新(macOS 14+)API,允许用户使用手势(按下词干)来静音音频输入.
According to WWDC,这样做有两个"级别":获取通知并在应用程序级别处理它,或者使用较低级别的CoreAudioAPI.在这种情况下,我试图做前者.
这是我的示例代码(相关部分只有Manager
个--其余部分只是大量的样板文件,用于通过CoreAudio获取麦克风输入,因此这可以作为最小的可复制示例).
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 {}
}
}
Note: if you attempt to run this, make sure to add Audio Input to the Capabilities of the app and the 100 key to the 101
When I press the stem on my AirPods Pro (2nd generation), I get this:
How can I make sure that either the 100 or 101 actually get called when the stem is pressed?