ASIO音频驱动开发指南 2.0

1.0版和2.0版的PDF下载地址:http://bbs.driverdevelop.com/read.php?tid-111697-keyword-asio.html

2.0版本在原文档基础上,增加实现了一款ASIO音频软件,讲述其实现代码和内部逻辑。原文在本博客中搜索,本文只包含新增部分。

注释(2011/2/22):

我新出版的《竹林蹊径——深入浅出Windows驱动开发》一书中,有一章专门阐述了ASIO驱动开发,包括完整的用户层ASIO接口驱动和内核ASIO驱动实现,它不需要实际硬件,即能完整实现ASIO功能并工作,这种形式的ASIO被我定义为V-ASIO。由于书刚出版,我还不能把文档免费发布,但今年晚些时候,我会争取发出来。请大家关注。下图是V-ASIO原理图:

图1 V-ASIO原理图

附录. 简易ASIO音频播录软件

图2 ASIO软件界面

上面章节讲了IASIO接口类中的接口函数。和读SDK文档中的介绍文字一样,看过之后可能会觉得有一种看得见、但摸不着的感觉。这个一个小节,我用一个音频软件的简例,力争让大家有机会能摸得着这些接口。

简单介绍一下这个例子程序。这是一个用Win32控制台编写的简易ASIO音频软件,能够实现ASIO的播录功能。它首先根据指定的ASIO驱动名称查找并获取ASIO驱动类的一个实例指针。然后初始化这个实例,并在用户控制下进行播放或录音操作。

代码大体改编自SDK中的sample,但根据我收到的读者反馈,SDK中的sample有几个很不好的地方。第一是写得太标准了(一笑),以至于好多读者都不耐心去分析那一个子函数套一个子函数的结构;第二是竟然用把数字打印到屏幕上的方式来模拟音频输出,要知道大部分读者是没有耐心的,可能还急眼啦,他们需要的,是能够直接听到声音!

我在重写的过程中,针对第一点,尽量把代码写得简单,在主函数中完成了大部分的初始化工作;针对第二点(读者会微笑的),我使得它既能播放,又能录音。

这个例子程序可运用于任何ASIO驱动,本人在XP系统上用AudioPhile、麦盒(Mi-Box I)测试过,也试过ASIO4ALL,完美通过。如果读者的系统中已经安装有某个ASIO驱动,也可试试。希望这个例子不是美人图,而是美人胚,让你看到且摸到。

5.1 主函数

主函数包含了最主要的内容,全部ASIO驱动初始化都在主函数中完成。我们需要获取IASIO接口类的一个实例,初始化实例,然后获取ASIO驱动的有用信息。我发现代码并不多,所以就全部粘贴过来,代码中详细的注释足以让读者读懂它们。

int _tmain(int argc, _TCHAR* argv[])

{

ASIOError result;

char strErr[128];

ASIOCallbacks asioCallbacks;

__try

{

//

// 获取ASIO驱动接口类的实例;这是第一步工作,如果成功才能进行下面的初始化工作

//

gpASIO = loadDriver(argv[1]);

if(gpASIO == NULL){

printf(“找不到ASIO设备/n”);

__leave;

}

//

// 初始化

//

result = gpASIO->init(NULL);

if(ASIOTrue != result)

{

gpASIO->getErrorMessage(strErr); // 获取错误信息

printf(“调用init方法失败:%d %s”, result, strErr);

__leave;

}

// 获取ASIO驱动名称

gpASIO->getDriverName(gASIODrvInfo.driverInfo.name);

gASIODrvInfo.driverInfo.driverVersion = gpASIO->getDriverVersion();

printf (“/n名称: %s/n版本: %d/n”,

gASIODrvInfo.driverInfo.name,

gASIODrvInfo.driverInfo.driverVersion

);

// 获取缓冲信息

result = gpASIO->getBufferSize(&gASIODrvInfo.minSize,

&gASIODrvInfo.maxSize,

&gASIODrvInfo.preferredSize,

&gASIODrvInfo.granularity

);

if(result != ASE_OK)

{

gpASIO->getErrorMessage(strErr); // 获取错误信息

printf(“调用getBufferSize方法失败:%d %s”, result, strErr);

__leave;

}

// 获取当前采样频率

result = gpASIO->getSampleRate(&gASIODrvInfo.sampleRate);

if(result != ASE_OK)

{

gpASIO->getErrorMessage(strErr); // 获取错误信息

printf(“调用getSampleRate方法失败:%d %s”, result, strErr);

__leave;

}

// 判断获取的当前采样频率值是否在有效区间

if (gASIODrvInfo.sampleRate <= 0.0 || gASIODrvInfo.sampleRate > 96000.0)

{

result = gpASIO->setSampleRate(44100.0);

if(ASE_OK != result)

{

printf("不支持默认采样频率:%d", result);

__leave;

}

result = gpASIO->getSampleRate(&gASIODrvInfo.sampleRate);

if(result != ASE_OK)

{

gpASIO->getErrorMessage(strErr); // 获取错误信息

printf("调用getSampleRate方法失败:%d %s", result, strErr);

__leave;

}

}

// 获取输入/输出声道数

result = gpASIO->getChannels(

&gASIODrvInfo.inputChannels,

&gASIODrvInfo.outputChannels

);

if(result != ASE_OK)

{

gpASIO->getErrorMessage(strErr); // 获取错误信息

printf(“调用getChannels方法失败:%d %s”, result, strErr);

__leave;

}

// 针对每个声道,初始化ASIO缓冲

//

ASIOBufferInfo *info = gASIODrvInfo.bufferInfos;

// input

if (gASIODrvInfo.inputChannels > kMaxInputChannels)

gASIODrvInfo.inputBuffers = kMaxInputChannels;

else

gASIODrvInfo.inputBuffers = gASIODrvInfo.inputChannels;

for(int i = 0; i < gASIODrvInfo.inputBuffers; i++, info++)

{

info->isInput = ASIOTrue;

info->channelNum = i;

info->buffers[0] = info->buffers[1] = 0;

}

// outputs

if (gASIODrvInfo.outputChannels > kMaxOutputChannels)

gASIODrvInfo.outputBuffers = kMaxOutputChannels;

else

gASIODrvInfo.outputBuffers = gASIODrvInfo.outputChannels;

for(int i = 0; i < gASIODrvInfo.outputBuffers; i++, info++)

{

info->isInput = ASIOFalse;

info->channelNum = i;

info->buffers[0] = info->buffers[1] = 0;

}

// 初始化回调函数数组。这些函数必须由音频程序自己提供,被ASIO驱动调用。

// bufferSwitch是实现音乐播放的关键,此处传入音乐文件数据,就能播放出相应的音乐了。

// sampleRateChanged在设备采样率改变时被调用,以提醒播放软件更改显示信息。

// bufferSwitch和bufferSwitchTimeInfo两个回调函数,ASIO驱动在需要更新音频数据的时候调用它们。

// 后者是前者的升级版,即ASIO驱动在调用的时候会传入一个时间信息结构指针,便于音频软件做同步、定位等操作。

// asioMessages回调可以让ASIO驱动更好地了解音频软件的“能力”,比如它所能支持的ASIO版本,是1.0还是2.0等。

asioCallbacks.bufferSwitch = &bufferSwitch;

asioCallbacks.sampleRateDidChange = &sampleRateChanged;

asioCallbacks.asioMessage = &asioMessages;

asioCallbacks.bufferSwitchTimeInfo = &bufferSwitchTimeInfo;

// 创建缓冲区,注册回调函数

result = gpASIO->createBuffers(gASIODrvInfo.bufferInfos,

gASIODrvInfo.inputBuffers + gASIODrvInfo.outputBuffers,

gASIODrvInfo.preferredSize,

&asioCallbacks

);

if(result != ASE_OK)

{

gpASIO->getErrorMessage(strErr);

printf(“调用createBuffers方法失败:%d %s”, result, strErr);

__leave;

}

// 调用createBuffers后,可获取设备的输入输出延迟。

// 延迟是一定存在的,除非操作系统能够快到收到一个字节,立刻播放一个字节的速度。

// 既然有缓冲区,就有先后、等待。等待的时间就是延迟。

// 输入延迟:一个音频采样从被设备获取到被传入音频软件,所经历的时间

// 输出延迟:一个采样数据从音频软件输出去,直到它被声卡播放出来,期间所经历的时间

gpASIO->getLatencies(&gASIODrvInfo.inputLatency, &gASIODrvInfo.outputLatency);

// 获取声道的详细信息

printf(“声道信息…/n/n输入声道:%d个/t输出声道:%d个”, gASIODrvInfo.inputBuffers, gASIODrvInfo.outputBuffers);

for (int i = 0; i < gASIODrvInfo.inputBuffers + gASIODrvInfo.outputBuffers; i++)

{

gASIODrvInfo.channelInfos[i].channel =

gASIODrvInfo.bufferInfos[i].channelNum;

gASIODrvInfo.channelInfos[i].isInput =

gASIODrvInfo.bufferInfos[i].isInput;

result = gpASIO->getChannelInfo(&gASIODrvInfo.channelInfos[i]);

if (result == ASE_OK)

printf("%d %s声道%d:%s group: %d type: %d/n", 

 i, gASIODrvInfo.channelInfos[i].isInput ? "输入":"输出",

 gASIODrvInfo.channelInfos[i].channel,

 gASIODrvInfo.channelInfos[i].name,

 gASIODrvInfo.channelInfos[i].channelGroup,

 gASIODrvInfo.channelInfos[i].type);

}

printf(“/n音频信息…/n/n”);

printf( “采样率:%d/n”, gASIODrvInfo.sampleRate);

printf( “输入延迟:%d, 输出延迟:%d/n”,

gASIODrvInfo.inputLatency, gASIODrvInfo.outputLatency);

printf( “最大缓冲:%d, 最小缓冲:%d, 当前缓冲:%d, 缓冲粒度:%d/n”,

gASIODrvInfo.maxSize, gASIODrvInfo.minSize,

gASIODrvInfo.preferredSize, gASIODrvInfo.granularity);

// 测试ASIO驱动是否支持outputReady接口。

// 如果ASIO驱动不支持,按照SDK中所做说明,返回值当为ASE_NotPresent;

// 这种情况下,我们程序下面就不应当调用outputReady与ASIO驱动进行数据同步。

if(ASE_OK == gpASIO->outputReady())

gASIODrvInfo.postOutput = TRUE;

else

gASIODrvInfo.postOutput = FALSE;

InitializeCommandVaribles();

// 提高线程优先级

SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);

// 等待并处理用户命令,直到退出。

while (!gASIODrvInfo.bMainQuit)

{

CommandProcess();

}

}

__finally

{

if(gpASIO)

{

unloadASIO(gpASIO);

delete gpASIO;

}

if(data_buff)

delete data_buff;

}

return 0;

}

最后我拎出几个要点讲一下:

  1. 音频软件由于数据吞吐量大,并且ASIO又要求低延迟和高性能保证,所以需要提高数据处理线程的优先级。这样处理后的效果是相当明显的;读者可尝试把提高和不提高优先级两种情况进行比较:把ASIO驱动的延迟降到最低,然后修改代码分别在提高优先级和不提高优先级的情况下运行软件。经过我多次测试后发现,不提高优先级的时候爆音情况普遍严重。

  2. ASIO驱动初始化函数的调用需要注意先后顺序。比如一般只在createBuffers被调用后才能调用getLatencies获取延迟值。读者应阅读SDK中的说明。

  3. 四个ASIO回调函数非常重要。音频软件向ASIO驱动注册这些函数,自己是不会调用的,而是由ASIO驱动在适当的时候调用。初始化结束后,剩余的工作大部分都是由这几个回调在操作着。另外一个要点是,回调函数的调用是不被音频软件控制的,它们的运行环境不可知,可能在主线程中,也可能在辅助线程中。所以要注意线程同步。

5.2 数据同步

数据同步的重任落在bufferSwitch或bufferSwitchTimeInfo两个回调函数的肩膀上。bufferSwitchTimeInfo函数比bufferSwitch多接受一个时间戳参数。软件可以根据时间戳,更新界面上显示的进度。

数据同步是以声道为单位进行的:先处理声道0数据,再处理声道1数据…但音频文件一般都是双声道的,所以可以变通一下将声道0和声道1组成立体声同时处理,这样更方便。可参看代码中的实现。

数据同步需要判断采样类型,采样类型其实就是采样深度,有16位、24位、32位,以及大序(Big Edian),小序(Little Edian)等。如声道0的采样类型为32位,当音频文件格式为16位采样深度时,需将文件中的每个采样转换为32位后再拷贝到声道0的缓冲中。转换算法算不上复杂,但如果弄错了,就会听到噪音。

播放的时候,将音频文件或内存中的数据拷贝到ASIO驱动的声道缓冲中;录音的时候,将声道缓冲中的数据保存到文件或内存中。这样,就实现了音频播录。

5.3 其他注意点

  1. 流程控制。ASIO数据流总是输入/输出操作并行的,所以不管是启动播放操作,还是启动录音操作,都将把两个方向的数据流同时开启。而如果要彻底关闭ASIO数据流,需要同时关闭播放与录音。

  2. 音频格式。播放音频文件就会涉及到文件读写,正确解析WAV音频格式很重要。重要的信息包括:采样深度、采样频率、声道数等。

  3. 用户命令。软件接受用户输入的简单命令,输入h或?命令,会打印出帮助信息。软件运行方法:asioTool [ASIOName]。如果参数ASIOName为空,软件将把系统中找到的第一个ASIO驱动作为当前驱动。

用户命令包括:P-播放;R-录音;T-停止;Q-退出;S-设置;h/?-帮助

到这里,文章就告一段落了。本文介绍了ASIO驱动的一种简单实现,并利用DirectKS技术对普通的WDM声卡提供了ASIO支持,最后还让你成功听到了音乐。我又在附件中为读者展示了一款微型ASIO音频软件,讲解了其代码实现和内部逻辑。通过这些讲解,ASIO从里到外的知识都能串起来了。
————————————————
原文链接:https://blog.csdn.net/blog_index/article/details/5411694

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值