关于 OpenSL ES 的那些事

发布于:2024-10-24 编辑:匿名 来源:网络

背景介绍 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(sampleRate) * ;engine.fastPathFramesPerBuf_ = static_cast(framesPerBuf); engine.sampleChannels_ = AUDIO_SAMPLE_CHANNELS;engine.bitsPerSample_ = SL_PCMSAMPLEFORMAT_FIXED_16;//新建对象 result = slCreateEngine(&engine.slEngineObj_, 0, NULL, 0, NULL, NULL);SLASSERT(result);//初始化 result = (*engine.slEngine Obj_ )->Realize(engine.slEngineObj_, SL_BOOLEAN_FALSE);SLASSERT(result);//获取引擎接口,以便引擎可以用来构建其他对象 result = (*engine.slEngineObj_)->GetInterface(engine. slEngineObj_, SL_IID_ENGINE, &engine.slEngineItf_);SLASSERT(result);//计算建议的最快音频缓冲区大小//低延迟需要以下两点//缓冲区应尽可能小(此处调整)AND//从记录器接收到缓冲数据后,在播放之前最小化数据缓冲时间 // 调整缓冲区大小以满足您的要求 [before itbusts]bufSize = engine.fastPathFramesPerBuf_ * engine.sampleChannels_ * engine.bitsPerSample_;bufSize = (bufSize + 7) >> 3;// bits --> byteengine.bufCount_ = BUF_COUNT;engine.bufs_ = allocateSampleBufs(engine.bufCount_, bufSize);assert(engine.bufs_);//释放缓冲区和接收缓冲区engine.freeBufQueue_ = new AudioQueue(engine.bufCount_) ;engine.recBufQueue_ = new AudioQueue (engine.bufCount_);assert(engine.freeBufQueue_ && engine.recBufQueue_);for(uint32_t i=0; ipush( &engine.bufs_ [i]);} 引擎对象slEngineObj创建后就无法使用。需要实现通过该对象获取引擎接口。

可以通过引擎接口来获取后面要用到的播放。记录接口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层的声音采集、传输和播放过程。

如果您有更好的优化建议,请向我们提出您的建议。

关于 OpenSL ES 的那些事

站长声明

版权声明:本文内容由互联网用户自发贡献,本站不拥有所有权,不承担相关法律责任。如果发现本站有涉嫌抄袭的内容,欢迎发送邮件 举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。

标签:

相关文章

  • 自动驾驶初创公司Momenta获得4600万美元B轮投资,蔚来资本领投

    自动驾驶初创公司Momenta获得4600万美元B轮投资,蔚来资本领投

    据投资界7月25日消息,自动驾驶初创公司Momenta获得4600万美元B轮投资,由蔚来资本领投,戴姆LE集团(梅赛德斯-奔驰母公司)、顺为资本、创新工场、九合创投也参与投资。   此前,今年11月,Momenta公司获得蓝湖资本领投的A轮融资,创新工场、真格基金跟投。 年初获得顺为资

    06-18

  • 郑州需要各方支持,未来法拉第合并上市

    郑州需要各方支持,未来法拉第合并上市

    7月20日,郑州遭遇特大暴雨,道路淹水严重,雨水涌进地铁,成为了一个令人心惊胆战的夜晚。 无数郑州人的心。 在灾难面前,一个人特别渺小,但在一起就很伟大。 昨天晚间,腾讯公益基金会率先紧急公布第一批1亿元捐款,并在其平台设立“河南抗洪救助”版块,为全国网友开通捐

    06-17

  • 为什么视觉中国不敢加图

    为什么视觉中国不敢加图

    被告拍的照片侵权?如果对方是视觉中国,似乎也没什么奇怪的。 2016年,视觉中国发布首张全人类共享的黑洞照片,并因声称拥有国旗、国徽图像版权而受到批评。 近日,在广撒维权的同时,遭遇了该图片的“亲生父亲”。 李鬼去了李逵,但子弹还要飞一段时间。 我们先按照时间线回

    06-21

  • 2023中国游戏产业年会暨ChinaJoy展会二十周年庆典在广州隆重举行

    2023中国游戏产业年会暨ChinaJoy展会二十周年庆典在广州隆重举行

    国家新闻出版总署指导,广东省新闻出版局、广州市委宣传部支持中共中央、中国音像数字出版协会、广州开发区主办,广州市黄埔区管委会、人民政府主办,中国音乐与数字协会游戏工作委员会、宣传部承办中共广州市黄埔区委、黄埔文化(广州)发展集团有限公司、黄埔文商旅游(广州

    06-18

  • 优衣库首次在门店出售咖啡,每杯12元

    优衣库首次在门店出售咖啡,每杯12元

    逛累了喝一杯咖啡,陪孩子和哆啦A梦比赛,走前带上一束鲜花。 所有这些体验都可以在优衣库银座店一站式实现。 9月17日,优衣库位于日本东京银座的全球旗舰店装修后重新开业。 这家已有10年历史的12层店位于豪华购物中心GINZA SIX的正对面。 ▲ 图片来自:日经中文网 这家店改

    06-21

  • 疫情过后,医疗健康该如何投资?

    疫情过后,医疗健康该如何投资?

    疫情期间如何投资VC/PE?近日,前海深港基金小镇与投资界在深圳前海深港基金小镇路演中心联合举办了主题为“疫情下的投资天堂——医疗健康”的论坛。 前海深港基金小镇董事长蔡杰、深圳创投健康产业基金投资部总经理周毅、高特嘉研究部高管合伙人张鹏、华大共赢投资合伙人纪昌

    06-18

  • 有消息称开心网赴美上市或遇阻,被指错失最佳时机

    有消息称开心网赴美上市或遇阻,被指错失最佳时机

    7月15日,投资界消息称,前国内SNS明星公司开心网遭遇其赴美上市遭遇重重阻碍,错失最佳时机。 首次公开募股的机会。   上述消息人士表示,开心网由于自身流量下降、资本市场环境不佳以及中概股诚信问题,在申请IPO过程中遇到了很大困难,SEC (美国证券交易委员会)开心网

    06-18

  • 纪念查理·芒格|蓝驰分享

    纪念查理·芒格|蓝驰分享

    纪念查理芒格|蓝驰分享笔记 蓝驰创投 蓝驰创投 微信ID蓝池创投关于特色 蓝驰创投中国成立于2016年,专注于早期风险投资,管理资金超亿元,累计投资超亿元。 创业企业,是理想汽车、水滴公司、青云、瓜子二手车、怪兽充电、云图、宏景智家、云升智能、百图生物等优秀企业的早

    06-18

  • 首次发布 -瑞讯生物完成数千万元C1轮融资,产品已进入多家三级医院

    首次发布 -瑞讯生物完成数千万元C1轮融资,产品已进入多家三级医院

    投资界(ID:pedaily)11月17日消息,近日,基于微流控自研技术平台,深耕微流控诊断与生命科学公司领导者瑞讯生物完成数千万元C1轮融资。 本轮融资由千岛基金领投,皓月资本担任本轮融资独家保荐人。 瑞讯生物成立于2007年,位于苏州工业园区。 其在美国硅谷生命科学产品线拥

    06-17

  • 植物蛋白饮料品牌“燕麦”完成数千万元A轮融资,由五源资本领投

    植物蛋白饮料品牌“燕麦”完成数千万元A轮融资,由五源资本领投

    植物蛋白饮料品牌“燕麦”完成数千万元A轮融资,领投婺源资本。 股东华创资本和imo Ventures也参与了投资,其中棕榈资本为财务顾问。 本轮融资将主要用于线下渠道拓展和品牌建设。

    06-17

  • 深圳有一款游戏出现在98%的朋友圈里,你确定不想约会吗?

    深圳有一款游戏出现在98%的朋友圈里,你确定不想约会吗?

    生活中的种种诱惑,存在于每一句话中。 比如:有的人做的家常菜生动鲜美,有的人28天练出人鱼线,有的人一年加薪好几次,年纪轻轻年薪50万+。 有些人可以朝九晚五工作并环游世界。 过着耀眼而美好的生活……当你在生活中彷徨,陷入各种诱惑时,突然有一股力量从千里之外传来。

    06-18

  • 人力资源企业服务商“礼才网”完成3亿元C1轮融资

    人力资源企业服务商“礼才网”完成3亿元C1轮融资

    8月6日消息,“礼才网”完成3亿元C1轮融资。 中投信联、广信资本、中南泊富基金、诚煜资本、德信基金等投资,创始人和CEO陈谏也参与投资。 融资金额将用于PaaS+小生态开发以及基于AI的HR专业管理产品的研发。 “理才网”成立于2007年,是一家以人力资源服务为核心的企业服务

    06-18