自动驾驶初创公司Momenta获得4600万美元B轮投资,蔚来资本领投
06-18
背景介绍 OpenSL ES 是专门针对嵌入式系统优化的硬件音频加速 API。它没有许可费用,并且可以跨平台使用。
它提供的高性能、标准化和低延迟功能实现为嵌入式媒体开发提供了标准。嵌入式开发人员开发本地音频应用程序也将变得更加容易。
该API可用于实现软件/硬件音频性能的提升。直接跨平台部署,降低执行难度,促进高级音频市场的发展。
OpenSL ES 框架图 硬件实现: 软件实现: Android 应用中的录音会出现延迟,声音输出到扬声器也需要一段时间。经测量,在大多数基于 ARM 和 x86 的设备上,音频 RTL 的延迟可低至毫秒,其中大多数应用程序是使用面向音频的 Android 方法开发的。
此延迟范围对于用户群来说是不可接受的,并且预期延迟必须低于毫秒。在大多数情况下,低于 20 毫秒是理想的 RTL。
还要考虑音频处理延迟和缓冲区队列总数。与其他 API 一样,OpenSL ES 使用回调机制工作。
在 OpenSL ES 中,回调仅用于通知应用程序新缓冲区可以排队(用于播放或录制)。在其他 API 中,回调还可以处理指向要填充或使用的音频缓冲区的指针。
但在 OpenSL ES 中,更可选的是,可以实现 API,以便回调作为信号机制运行,从而将所有处理保留在音频处理线程上。这将包括在接收到分配的信号后对所需的缓冲区进行排队。
OpenSL ES使用流程 之前研究电视卡拉OK的时候,有一个方案是从麦克风获取音频数据,但是使用系统的AudioRecord采集数据存在一定的延迟。虽然谷歌在5.0之后对音频做了一定的优化,延迟略有改善,但效果仍然差强人意。
因此,为了获得更好的聆听效果,OpenSL ES是最合适的。主要原因有以下三点。
OpenSL ES 使用的缓冲队列机制使其在 Android 媒体框架中更加高效。如果手机支持低延迟功能,则需要使用 OpenSL ES(google 原文:Low-latency audio is only support when using Android'simplementation of the OpenSL ES? API and the Android NDK.)由于实现是本机代码,因此它可以提供更高的性能,因为本机代码不受 Java 或 Dalvik VM 开销的影响,因此这种方法有助于基于 Android 的音频开发。
以下是OpenSL ES的初始化流程图。 OpenSL ES中的所有操作都是通过接口完成的。
与Java接口类似,接口提供底层方法调用。常用的接口如下: SLObjectItf:对象接口 SLEngineItf:引擎接口 SLPlayItf:播放接口 SLBufferQueueItf:缓冲队列接口 SLVolumeItf:音量接口分为初始化、音频数据采集、音频数据传输、音频数据播放四个部分。
初始化 初始化主要包括OpenSL ES引擎初始化和录音/播放器初始化。 OpenSL ES引擎初始化 OpenSL ES引擎初始化的要点是创建一个新的引擎对象来连接JNI并与底层交互,设置引擎的采样参数,包括采样扁平率、采样帧大小、采样通道和采样深度,并初始化音频数据缓冲区。
队列。需要注意的是,本次实验中使用的发送端和服务端的采样参数需要设置相同。
代码语言:javascript copy SLresult result;memset(&engine, 0, sizeof(engine));//设置采样参数 engine.fastPathSampleRate_ = static_cast
可以通过引擎接口来获取后面要用到的播放。记录接口fastPathFramesPerBuf是每个buffer缓冲区的采样点数,整个bufsize的大小是所有通道采样点数的两倍,因为采样深度是16bit,也就是2个字节。
freeBufQueue指空闲缓冲队列,主要提供空采样数组。 recBufQueue是接收缓冲队列,主要用于存储采集到的音频数据,也是播放数据的来源。
引擎初始化后,会初始化freeBufQueue,并初始化16个空字节数组。这样就完成了音频引擎的初始化。
OpenSL ES Recorder 初始化 录音机的初始化主要是设置声音源、设置采集数据格式、获取采样缓冲队列和配置接口等,代码如下: 代码语言:javascript copy sampleInfo_ = *sampleFormat; SLAndroidDataFormat_PCM_EX format_pcm;ConvertToSLSampleFormat(&format_pcm, &sampleInfo_) ;//设置声音源 SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE,SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT,NULL };SLDataSource audioSrc = {&loc_dev, NULL };/ /设置音频数据池 SLDataLocator_AndroidSimpleBufferQueue loc_bq = { SL_DATALOCA TOR_ANDROIDSIMPLEBUFFERQUEUE, DEVICE_SHADOW_BUFFER_QUEUE_LEN };SLDataSink audioSnk = {&loc_bq, &format_pcm };//创建Recorder需要RECORD_AUDIO权限 const SLInterfaceID id[2] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION };const SLboolean 请求[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};结果 = ( *slEngine)->CreateAudioRe corder(slEngine,&recObjectItf_,&audioSrc , &audioSnk,sizeof(id)/sizeof(id[0]),id, req);//配置语音识别的默认值 SLAndroidConfigurationItf inputConfig;result = (*recObjectItf_)->GetInterface(recObjectItf_,SL_IID_ANDROIDCONFIGURATION,&inputConfig);if (SL_RESULT_SUCCESS ==结果) { SLuint32 presetValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; (*inputConfig)->SetConfiguration(inputConfig,SL_ANDROID_KEY_RECORDING_PRESET, &presetValue,sizeof(SLuint32));}//实现录音对象 result = (*recObjectItf_)->Realize(rec ObjectItf_, SL_BOOLEAN_FALSE);//获取录音接口结果= (*recObjectItf_)->GetInterface(recObjectItf_, SL_IID_RECORD, &recItf_);//获取录音队列接口 result = (*recObjectItf_)->GetInterface(recObjectItf_, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recBufQueueItf_);//注册录音队列回调 result = ( *recBufQueueItf_)->RegisterCallback(recBufQueueItf_, bqRecorderCallback, this);//初始化音频采集传输队列 devShadowQueue_ = new AudioQueue(DEVICE_SHADOW_BUFFER_QUEUE_LEN);首先定义声音源数据SLDataSource,它包含两个成员,DataLocator数据定位器和数据格式。数据格式一般采用比较常见的PCM数据。
数据定位器一般是指声音采集后的存储位置。对于四个midi缓冲队列位置、缓冲队列位置、输入/输出设备位置和内存位置,我们使用PCM数据进行此验证,并且为了更高效地操作和收集数据,使用缓冲队列的存储位置。
接下来是音频数据池的初始化。音频数据池是指数据输出。
主要设置Recorder需要设置的音频数据的输出位置和输出格式。初始化录音对象recObjectItf后,得到录音接口recItf。
稍后开始录制需要这个接口。 recBufQueueItf是记录队列的接口,通过它注册缓冲队列的回调接口。
OpenSL ES Player 初始化 Player 初始化与 Recorder 类似,主要是设置声音源、设置采集数据格式、获取采样缓冲队列和配置接口等。代码如下: 代码语言:javascript copy sampleInfo_ = *sampleFormat; //初始化OutputMix,用来输出声音数据 result = (*SLENGINE)->CreateoutPutmix(Slengine, &OutputMixobjectf_, 0, NULL, NULL); _)-> 实现(outputMixobjectF_, SL_BOOLEAN_FALSE); // 配置声音源数据 SLDATALOCATOR_ANDROIDSIMPELEBUFERQUEUEUE loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, DEVICE_SHADOW_BUFFER_QUEUE_LEN };SLAndroidDataFormat_PCM_EX format_pcm;ConvertToSLSampleFormat(&format_pcm, &sampleInfo_);SLDataSource audioSrc = {&loc_bufq, &format_p cm};//配置音频数据输出池 SLData Locator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObjectItf_ };SLDataSink audioSnk = {&loc_outmix, NULL };/* 初始化播放器 */SLInterfaceID ids[2] = { SL_IID_BUFFERQUEUE, SL_IID_VOLUME};SLboolean req[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};result = (*slEngine)->CreateAudioPlayer(slEngine, &playerObjectItf_, &audioSrc, &audioSnk,sizeof(ids)/sizeof(ids[0]), ids, req);//实现playerresult = (*playerObjectItf_)->Realize(playerObjectItf_, SL_BOOLEAN_FALSE) ;SLASSERT(result);//获取播放器接口 result = (*playerObjectItf_)->GetInterface(playerObjectItf_, SL_IID_PLAY, &playItf_);//获取音量接口 result = (*playerObjectItf_)->GetInterface(playerObjectItf_, SL_IID_VOLUME, &volumeItf_ ); //获取缓冲队列接口 result=(*playerObjectItf_)->GetInterface(playerObjectItf_,SL_IID_BUFFERQUEUE,&playBufferQueueItf_);//注册缓冲接口回调 result=(*playBufferQueueItf_)->RegisterCallback(playBufferQueueItf_, bqPlayerCallback, this);比较Recorder的初始化,还有一个OutputMix的额外初始化。
OutputMix主要用于向扬声器输出数据,因此可以认为是输出混音对象接口的初始化。最终得到的playBufferQueueItf是播放缓冲队列的接口,可以认为是队列。
Recorder中recBufQueueItf的数据来源是一致的。实际上就是将数据缓冲队列中的数据收集起来,通过Socket传输给playBufferQueueItf,供Player实现播放。
音频数据采集音频数据采集的主要过程是初始化缓冲队列、开始录音设置、最后开始录音。流程图如下:启动大小设置为2,开始录制前,将2个录制数组放入录制内存空间。
启动后,记录数据将被收集到这两个数组中。当录音数组满时,会触发上面Recorder中设置的回调。
在回调中,会将录制的声音数据取出并通过Socket发送出去。代码语言:javascript copy sample_buf *dataBuf = NULL;//采集到的音频数据数组 devShadowQueue_->front(&dataBuf);//获取采集到的数组 devShadowQueue_->pop();//删除表头 dataBuf->size_ = dataBuf ->cap_;//只有数组满后才回调,所以size可以设置为最大长度 sendUdpMessage(dataBuf);//使用UDP发送sample_buf* freeBuf;while (freeQueue_->front(&freeBuf) && devShadowQueue_ - >push(freeBuf)) { freeQueue_->pop();//删除已使用的空闲数组 SLresult result = (*bq)->Enqueue(bq, freeBuf->buf_, freeBuf->cap_);//继续下一步收集sample_buf的时间 *vienBuf = allocateOneSampleBufs(getBufSize()); freeQueue_->push(vienBuf);//添加一个新的空闲数组} 以上是回调中的代码。
首先devShadowQueue取出采集到的音频数据并发送出去。 ,并继续下一次采集。
这里使用while循环将尽可能多的数组放入采集缓冲区中,以保证空闲数组(用于存储麦克风采集到的数据)的连续性。这里的音频数据传输分为发送和接收。
发送比较简单,因为此时网络已经建立了连接,直接调用send即可。代码语言: javascript copy void sendUdpMessage(sample_buf *dataBuf){ sendto(client_socket_fd, dataBuf->buf_, dataBuf->size_, 0, (struct sockaddr *) &server_addr, sizeof(server_addr));} 接收部分主要是接收数据被放入播放缓冲区。
最好在开始播放前预先将一定量的声音数据存入播放缓冲区,以避免播放时获取不到数据。代码语言: javascript copy sample_buf *vien_buf = sampleBufs(BUF_SIZE);if( recvfrom( server_socket_fd, vien_buf->buf_, BUF_SIZE, 0, (struct sockaddr*) &client_addr, &client_addr_length) == -1){ exit(1);} if (getAudioPlayer() != NULL) { getRecBufQueue()->push(vien_buf); if (count_buf++ == 3) { getAudioPlayer()->PlayAudioBuffers(PLAY_KICKSTART_BUFFER_COUNT);其中getRecBufQueue获取播放缓冲队列,存放三个数组后,通知Player可以开始播放了。
接收到所需的缓冲数据后开始音频数据播放。这里调用的PlayAudioBuffers方法就是开始播放的方法。
代码语言:javascript copysample_buf *buf = NULL;if(!playQueue_->front(&buf)) { uint32_t totalBufCount;回调_(ctx_, ENGINE_SERVICE_MSG_RETRIEVE_DUMP_BUFS, &totalBufCount);中断;}if(!devShadowQueue_->push(buf)) { 中断; // PlayerBufferQueue 已满! ! }(*playBufferQueueItf_)->入队(playBufferQueueItf_,buf->buf_, buf->size_);playQueue_->pop(); //删除已播放数组 playQueue为播放队列。如果为空,则表示没有缓冲数据。
这里回调转到用于错误处理的地方。如果成功取出,则先存入传输队列,并传入调用playback的方法开始播放。
最后将播放队列中的已播放数组删除,然后播放完成后,就会进入Player播放队列中注册的回调。代码语言: javascript copysample_buf *buf;if(!devShadowQueue_->front(&buf)) { if(callback_) { uint32_t count; }回调_(ctx_, ENGINE_SERVICE_MSG_RETRIEVE_DUMP_BUFS, &count); } return;}devShadowQueue_->pop();buf ->size_ = 0;if(playQueue_->front(&buf) && devShadowQueue_->push(buf)) { (*bq)->Enqueue(bq, buf-> buf_, buf->size_); playQueue_->pop();} else{sample_buf *buf_temp = newsample_buf; buf_temp->buf_ = 新的 uint8_t [BUF_SIZE]; buf_temp->size_ = BUF_SIZE; buf_temp->cap_ = BUF_SIZE; (*bq)->入队(bq, buf_temp->buf_, BUF_SIZE); devShadowQueue_->push(buf_temp);} 在回调中,第一步是取出传输队列devShadowQueue中的播放数据。
如果有则正常删除,同时继续从播放队列playQueue中取出播放数组同时放入。在传输队列devShadowQueue中,devShadowQueue有两个功能。
一是保证播放的连续性,二是播放数据的临时存储点。如果当前网络延迟无法接收到播放数据,就会出现播放队列取不到数据的情况。
目前传入的是一个空数组,根据经验,你会发现声音在一定时间内会卡住。这里的逻辑是这样的。
我们需要继续优化,如何有效控制声音滞后也将大大提高用户体验。本次分享主要介绍JNI层的声音采集、传输和播放过程。
如果您有更好的优化建议,请向我们提出您的建议。
版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。
标签:
相关文章
06-17
06-21
06-21
06-18
06-18
06-18
06-18
最新文章
【玩转GPU】ControlNet初学者生存指南
【实战】获取小程序中用户的城市信息(附源码)
包雪雪简单介绍Vue.js:开学
Go进阶:使用Gin框架简单实现服务端渲染
线程池介绍及实际案例分享
JMeter 注释 18 - JMeter 常用配置组件介绍
基于Sentry的大数据权限解决方案
【云+社区年度征文集】GPE监控介绍及使用