如何检查已安装虚拟音频设备
在macOS虚拟音频设备是以插件(plugin)的形式被系统加载的,有两种方式可以查看当前已加载的音频插件有哪些?
1. 打开目录“/Library/Audio/Plug-Ins/HAL”,这里是所有插件可执行文件的安装目录
2. 更直接的方式,可以直接打开应用程序“音频MIDI设置”查看当前激活的音频设备
虚拟音频设备的应用场景
音频设备按功能分为两种:音频输入设备、音频输出设备
1. 自定义音频录制
比如我们开发了一个音频处理应用,比如“库乐队”,希望可以把处理的结果输出给其他的应用,就可以实现一个虚拟音频输入插件,通过该插件接收来自应用的音频结果。当其他应用需要使用我们的声音输出结果的时候,就可以把该虚拟音频输入设备作为录音设备实现声音获取,从而实现两个应用之间的声音透传。
2. 重定向音频输出
当我们需要把某个应用或系统声音重定向到我们想要的软件或者设备,就可以实现一个虚拟音频输出插件,接收来自其他应用或系统的声音,根据需要进行处理或者输出到指定的扬声器。比如投影工具MaxHub,就是通过虚拟音频输出插件将系统的声音投影到投屏设备上进行输出。
如何实现虚拟音频设备
1. 创建Bundle工程
音频插件使用Bundle模版创建工程。
修改后缀为driver,Build Settings -> Wrapper Extension
2. 创建Info.plist文件,并设置相关属性
有两个属性是和音频插件相关的,需要注意设置:
- CFPluginFactories,设置插件入口函数,是一个字典,只需要一个Entry,其中的key是一个UUID格式的随机串,value是入口函数的名称。
- CFPluginTypes,也是字典,用来关联入口函数,只需要一个Entry,其中key是固定的UUID,等于kAudioServerPlugInDriverInterfaceUUID(在CoreAudio.framework/AudioServerPlugin.h头文件中定义),value是数组,只需要设置第一个Entry为CFPluginFactories里面使用的key。
最后别忘记了在Build Settings -> Info.plist File 中设置Info.plist文件路径。
3. 实现ClinAudio_Create入口函数
这是一个工厂方法,需要根据TypeUUID返回一个DriverInterface的结构体,其中TypeUUID就是前面传入的kAudioServerPlugInDriverInterfaceUUID,DriverInterface结构体是按照指定顺序声明的一系列和插件功能相关的函数入口。
static AudioServerPlugInDriverInterface gAudioServerPlugInDriverInterface =
{
NULL,
ClinkAudio_QueryInterface, // 查询接口
ClinkAudio_AddRef, // 添加引用
ClinkAudio_Release, // 释放引用
ClinkAudio_Initialize, // 初始化接口
ClinkAudio_CreateDevice, // 创建音频设备
ClinkAudio_DestroyDevice, // 销毁音频设备
ClinkAudio_AddDeviceClient, // 添加设备客户端
ClinkAudio_RemoveDeviceClient, // 移除设备客户端
ClinkAudio_PerformDeviceConfigurationChange, // 执行设备配置变更
ClinkAudio_AbortDeviceConfigurationChange, // 终止设备配置变更
ClinkAudio_HasProperty, // 查询是否存在属性
ClinkAudio_IsPropertySettable, // 查询属性是否可以设置
ClinkAudio_GetPropertyDataSize, // 获取属性数据大小
ClinkAudio_GetPropertyData, // 获取属性数据
ClinkAudio_SetPropertyData, // 设定属性数据
ClinkAudio_StartIO, // 开始数据流传输
ClinkAudio_StopIO, // 停止数据流传输
ClinkAudio_GetZeroTimeStamp, // 获取零时时间戳
ClinkAudio_WillDoIOOperation, // 即将开始IO操作
ClinkAudio_BeginIOOperation, // 开始IO操作
ClinkAudio_DoIOOperation, // 执行IO操作
ClinkAudio_EndIOOperation // 结束IO操作
};
static AudioServerPlugInDriverInterface* gAudioServerPlugInDriverInterfacePtr = &gAudioServerPlugInDriverInterface;
static AudioServerPlugInDriverRef gAudioServerPlugInDriverRef = &gAudioServerPlugInDriverInterfacePtr;
void* ClinkAudio_Create(CFAllocatorRef inAllocator, CFUUIDRef inRequestedTypeUUID)
{
// 这是插件的工厂方法,需要根据类型返回系列的接口实现
#pragma unused(inAllocator)
void* theAnswer = NULL;
if(CFEqual(inRequestedTypeUUID, kAudioServerPlugInTypeUUID))
{
theAnswer = gAudioServerPlugInDriverRef;
}
return theAnswer;
}
其他的函数实现根据需要实现即可,下面给出几个基本函数示例实现:
可以参考官方例子进行实现:creating_an_audio_server_driver_plug-in
static HRESULT ClinkAudio_QueryInterface(void* inDriver, REFIID inUUID, LPVOID* outInterface)
{
HRESULT theAnswer = 0;
CFUUIDRef theRequestedUUID = CFUUIDCreateFromUUIDBytes(NULL, inUUID);
if(CFEqual(theRequestedUUID, IUnknownUUID) || CFEqual(theRequestedUUID, kAudioServerPlugInDriverInterfaceUUID))
{
pthread_mutex_lock(&gPlugIn_StateMutex);
++gPlugIn_RefCount;
pthread_mutex_unlock(&gPlugIn_StateMutex);
*outInterface = gAudioServerPlugInDriverRef;
}
else
{
theAnswer = E_NOINTERFACE;
}
CFRelease(theRequestedUUID);
return theAnswer;
}
static ULONG ClinkAudio_AddRef(void* inDriver)
{
ULONG theAnswer = 0;
pthread_mutex_lock(&gPlugIn_StateMutex);
if(gPlugIn_RefCount < UINT32_MAX)
{
++gPlugIn_RefCount;
}
theAnswer = gPlugIn_RefCount;
pthread_mutex_unlock(&gPlugIn_StateMutex);
return theAnswer;
}
static ULONG ClinkAudio_Release(void* inDriver)
{
ULONG theAnswer = 0;
pthread_mutex_lock(&gPlugIn_StateMutex);
if(gPlugIn_RefCount > 0)
{
--gPlugIn_RefCount;
}
theAnswer = gPlugIn_RefCount;
pthread_mutex_unlock(&gPlugIn_StateMutex);
return theAnswer;
}
static OSStatus ClinkAudio_Initialize(AudioServerPlugInDriverRef inDriver, AudioServerPlugInHostRef inHost)
{
OSStatus theAnswer = 0;
gPlugIn_Host = inHost;
CFPropertyListRef theSettingsData = NULL;
gPlugIn_Host->CopyFromStorage(gPlugIn_Host, CFSTR("box acquired"), &theSettingsData);
if(theSettingsData != NULL)
{
if(CFGetTypeID(theSettingsData) == CFBooleanGetTypeID())
{
gBox_Acquired = CFBooleanGetValue((CFBooleanRef)theSettingsData);
}
else if(CFGetTypeID(theSettingsData) == CFNumberGetTypeID())
{
SInt32 theValue = 0;
CFNumberGetValue((CFNumberRef)theSettingsData, kCFNumberSInt32Type, &theValue);
gBox_Acquired = theValue ? 1 : 0;
}
CFRelease(theSettingsData);
}
gPlugIn_Host->CopyFromStorage(gPlugIn_Host, CFSTR("box acquired"), &theSettingsData);
if(theSettingsData != NULL)
{
if(CFGetTypeID(theSettingsData) == CFStringGetTypeID())
{
gBox_Name = (CFStringRef)theSettingsData;
CFRetain(gBox_Name);
}
CFRelease(theSettingsData);
}
if(gBox_Name == NULL)
{
gBox_Name = CFSTR("ClinkAudio Box");
}
// calculate the host ticks per frame
struct mach_timebase_info theTimeBaseInfo;
mach_timebase_info(&theTimeBaseInfo);
Float64 theHostClockFrequency = (Float64)theTimeBaseInfo.denom / (Float64)theTimeBaseInfo.numer;
theHostClockFrequency *= 1000000000.0;
gDevice_HostTicksPerFrame = theHostClockFrequency / gDevice_SampleRate;
gDevice_AdjustedTicksPerFrame = gDevice_HostTicksPerFrame - gDevice_HostTicksPerFrame/100.0 * 2.0*(gPitch_Adjust - 0.5);
return theAnswer;
}
//更多示例代码可以参考官方实现:https://developer.apple.com/documentation/coreaudio/creating_an_audio_server_driver_plug-in?language=objc