文档章节

Android 实现边听边录音探究

叶大侠
 叶大侠
发布于 03/21 10:20
字数 2005
阅读 101
收藏 0
点赞 0
评论 0

你需要有一些关于音频的基本认识,如果你还不是很了解,建议先阅读前面两篇文章。

  1. 写给小白的音频认识基础
  2. Android上一种效果奇好的混音方法介绍

场景描述

音乐中只有一种声音有时候很单薄的,我们经常希望把不同的声音加在一起,但是在录制的时候我们需要严格同步起来,把两种声音的时差控制在听觉允许的范围内,才可能获得我们想要的结果。另外一点,在录制的时候,为了不把播放的声音和人声或者器乐声混到一块,通常都需要录制者带着耳机边听边录。

为了实现最终两个或者多个声音能非常好的契合到一起,除了要解决录音和播放的同步,还需要考虑到声音从手机传输到耳机上的延时。这个场景除了会出现在一些比较专业的音乐软件上,常用的 K 歌软件也不可避免会遇到这个问题。

一线希望:MediaSyncEvent?

先抛出结论:并不能解决问题~

肯定先从 SDK 入手,发现 AudioRecord 里面有个方法 startRecording(MediaSyncEvent syncEvent) , 再看了一遍文档, 仿佛在黑暗中看到了一丝光亮。

The MediaSyncEvent class defines events that can be used to synchronize playback or capture * actions between different players and recorders.

然而对于它的使用资料实在太少,stackoverflow 上有个提问是 0 回答:这里。翻了 Google 很久,最终在官方的 CTS (Compatibility Test Suite) 中找到了它的身影:在 AudioRecordTesttestSynchronizedRecord方法中。这里顺便提一下,这些单元测试是非常好实打实的官方学习资料,如果苦于找不到答案的时候,不妨来这里找找看。

研究完testSynchronizedRecord我们回来看看MediaSyncEvent它究竟是用来干嘛的?

MediaSycEvent 可以通过 MediaSyncEvent.createEvent() 进行构造,它支持两种事件类型。

    /**
     * No sync event specified. When used with a synchronized playback or capture method, the
     * behavior is equivalent to calling the corresponding non synchronized method.
     */
    public static final int SYNC_EVENT_NONE = AudioSystem.SYNC_EVENT_NONE;

    /**
     * The corresponding action is triggered only when the presentation is completed
     * (meaning the media has been presented to the user) on the specified session.
     * A synchronization of this type requires a source audio session ID to be set via
     * {@link #setAudioSessionId(int) method.
     */
    public static final int SYNC_EVENT_PRESENTATION_COMPLETE = AudioSystem.SYNC_EVENT_PRESENTATION_COMPLETE;

其实就只有一种,SYNC_EVENT_NONE 就相当于没有同步事件,常规的 AudioRecord.startRecording() 方法就是用的这个参数。从AudioRecordTest.testSynchronizedRecord 的测试用例中可以得知SYNC_EVENT_PRESENTATION_COMPLETE的作用其实是等AudioTrack播放完的瞬间才触发AudioRecord的录音,这明显和我们的需求是不通的,没想明白在哪些场景会有这个需求,Google 要专门提供这个一个参数,如果有想法的朋友可以给我留言。

CyclicBarrier 来帮忙

此路不通之后,我们需要另辟蹊径。在运动员比赛前,我们需要先让大家在同一线上等待,直到看到信号发出再一起出发。在这里,我们也需要让 AudioTrackAudioRecord 先在同一起跑线上等着,然后一起出发,各奔东西。Java 世界里面的CyclicBarrier就很合适做这件事情。

// play 和 record 两个同步线程
CyclicBarrier recordBarrier = new CyclicBarrier(2);

AudioTrack audioTrack;
AudioRecord audioRecord;

// UI Thread
public void start(){
    recordBarrier.reset();
    audioTrack.play();
    audioRecord.startRecording();
    new RecordThread().start();
    new PlayThread().start();
}

class RecordThread extends Thread{
    public void run(){
        //等play线程开始写的时候read
        recordBarrier.await();
        audioRecord.read();
    }
}

class PlayThread extends Thread{
    public void run(){
        //等reacord线程开始读的时候write
        recordBarrier.await();
        audioTrack.write();
    }
}

上面通过CyclicBarrierAudioTrackwriteAudioRecordread 在同一起跑线上,似乎事情已经解决了,然而并没有。虽然你开始往耳机write数据,但是耳机接收到信号真正发出声音还要一段时间。

处理录音延时问题

我们回到用户真实的使用场景中,来看看问题是如何发生的?

录音延时

播放源是真实的数据源,比如位于 1ms 的伴奏数据块从写入AudioTrack开始到耳机播放可能已经是 100ms 后的事情了,而用户这个时候才开始录入自己的声音,这里还可能会有从设备开始采集声音到缓冲区的一个延时,如果是使用蓝牙耳机的话,那延时的问题就会更加突出了。

我们来感受一下延时的情况,在咖啡馆录的音,杂音比较多,但是不难听出来录音是比原来的声音要延迟了。

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/audio-sync-test-1.mp3" /> </audio>

看下声波图:

延迟声波图

解决方案:

当录音和播放开始之后,它们就会在同一时域中平行演绎,根据延时的特点,我们不难得出:

录音时长 = 延迟时长 + 播放时长 + 额外时长(播放完之后的自由录音)

只要我们能知道延迟的时长,在读取录音数据的时候,我们只要截取掉 AudioRecord 前面的延迟数据就可以让问题得到解决了。那怎么才能知道应该截掉多少个 byte 的数据呢?在这里我想到了一个巧妙的解决方法,给大家分享一下思路。

从上面的节拍器的声波图我们可以看到,波峰对应的就是的那一声,录音音轨和节拍器音轨上的波峰差就是我们想知道的延迟时长。根据这个特点,我们可以设计出获取这个延迟时长的一个思路:

  1. 让用户带上耳机,根据固定节奏的节拍器(要有一定时间间隔)声音进行录音,简单的啦..啦..啦..就好。
  2. 根据获取到的录音数据和原始的节拍器声音进行比较, 我取的是 8 个波峰区间数据进行比较,如果延迟误差都在一个小范围内,那就认为是正确的。

具体的算法大概如下:

//ANALYZE_BEAT_LEN = 8
int[] maxPositions = new int[ANALYZE_BEAT_LEN];
for(i = 0; i != maxPositions.length; i++){
    byte[] segBytes = getSegBytes(); //获取一拍时长的数据
    maxPositions[i] = getMaxSamplePos(segBytes);// 获取拍中波峰所在的大致位置
}

//按小到大排序
Arrays.sort(maxPositions);

//取中间一半的值,如果平均值误差在 10 毫秒内,就认为是正确的
int sampleTotalValue = 0;
int sampleLen = ANALYZE_BEAT_LEN / 2;
int[] sampleValues = new int[sampleLen];

for(int beginIndex = sampleLen / 2, i=0; i != sampleLen; i++){
    sampleValues[i] = maxPositions[ i + beginIndex];
    sampleTotalValue += sampleValues[i];
}

int averSampleValue = sampleTotalValue / sampleLen;

boolean isValid = true;
for(int sampleValue : sampleValues){
    //errorRangeByteLen : 10 毫秒的 byte 长度
    if(Math.abs(averSampleValue - sampleValue) > errorRangeByteLen){
        isValid = false;
    }
}

if(isValid){
    stopPlay = true;
    // 结果
    int result = averSampleValue;
}

结果展示

波形图:

声音结果:

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/audio-sync-test-2.mp3" /> </audio>

调整之后情况就改善多了,听觉上基本感受不到延迟了。但是这样会给用户带来一些不方便,换耳机的时候需要重新调整。个人的认知实在有限,虽然这可能是个有效的方法,但肯定不是最佳的做法,同时好奇像唱吧这种软件是如何处理的?欢迎大牛们交流一下想法~

参考资料

  1. 无线音频的延时问题:http://www.memchina.cn/News/9733.html

  2. MediaSyncEvent TestCase:

技术交流群:70948803,大部分时间群里都是安静的,只交流技术相关,很少发言,不欢迎广告喷子。

不玩音乐的看到这里可以关闭了。

色彩浓重的广告时间:

如果你有玩音乐,我做了一个音乐学习和记录的辅助工具。终于可以在 App 市场下载了: 声音笔记+,虽然还比较粗糙,期待你的支持~

还有这些好用的工具:

© 著作权归作者所有

共有 人打赏支持
叶大侠

叶大侠

粉丝 56
博文 44
码字总数 67312
作品 5
广州
程序员
TensorFlow Lite+Android,Google要搞的大事情

近日谷歌开源了TensorFlow的终端版本TensorFlow Lite,这个版本的发布其实早在预料之中,但又能从这件事看出未来谷歌整个生态版图的一些端倪,接下来就让博主为大家分析一下。 首先为什么说T...

gshengod ⋅ 2017/11/16 ⋅ 0

React-Native 之 环境配置和简单使用

前言 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习 本人接触 React Native 时间并不是特别长,所以对其中的内容和性质了解可能会有所偏差,在...

postdep ⋅ 05/01 ⋅ 0

Android 使用AudioRecord录音相关和音频文件的封装

在Android中录音可以用MediaRecord录音,操作比较简单。但是不够专业,就是不能对音频进行处理。如果要进行音频的实时的处理或者音频的一些封装 就可以用AudioRecord来进行录音了。 这里给出...

晨曦之光 ⋅ 2012/03/15 ⋅ 0

Android P Beta 2 及终版 API 强势来袭!

在四周前的 Google I/O 开发者大会上,我们发布了Android P 的首个 Beta 版,将人工智能 (AI) 定位为操作系统的核心,并侧重于提供智能且简洁的体验。 今天,我们隆重推出 Android P Beta 2...

谷歌开发者 ⋅ 06/07 ⋅ 0

Android MVP框架学习实践

五五六六七七八八 作为一名大三在读的学生,想着自己的大学生活只剩下一年了,有了些许紧迫感,于是就打算找一份实习工作来增加自己的项目实战经验。 想着互联网公司都基本都有现成的项目,实...

reggie1996 ⋅ 05/20 ⋅ 0

张高兴的 Xamarin.Android 学习笔记:(一)环境配置

  最近在自学 Xamarin 和 Android ,同时发现国内在做 Xamarin 的不多。我在自学中间遇到了很多问题,而且百度到的很多教程也有些过时,现在打算写点东西稍微总结下,顺便帮后人指指路了。...

张高兴 ⋅ 2017/01/13 ⋅ 0

Android Audio BSP工程师 需要清楚的基本知识点

原址 【前言】 这是我根据自己的工作经验所总结,有一定代表性,但可能不齐全。这只是个提纲。 【知识点】 按照在操作系统中的层次划分,从上到下依次为: 1、Android系统分层模型。知道 AP...

u010164190 ⋅ 05/30 ⋅ 0

Android界面跳转时候的生命周期

今天在开发的时候碰到一个坑。关于两个界面跳转的时候,生命周期顺序的问题。 场景模拟:比如我们有两个Activity。第一个界面展示排行榜,第二个界面是游戏(H5游戏)。我们玩游戏玩完一局以后...

tianshuai4317618 ⋅ 04/18 ⋅ 0

支持升级 Android P Beta 的机型

在美国时间5.8日的 IO 2018大会上,Google 官方正式宣布部分设备支持升级到 Android P Beta (安卓9.0系统)尝鲜。 那么具体哪些设备支持呢,可以看下面的清单: Google Pixel内置了 Google...

alice_tl ⋅ 05/11 ⋅ 0

基于JSON RPC的一种Android跨进程调用解决方案了解一下?

基于JSON RPC的一种Android跨进程调用解决方案了解一下? 基于JSON RPC的一种Android跨进程调用解决方案了解一下? 简介 今天上午,看票圈有朋友分享爱奇艺的跨进程通信框架——Andromeda,觉...

流水不腐小夏 ⋅ 05/30 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

浅谈springboot Web模式下的线程安全问题

我们在@RestController下,一般都是@AutoWired一些Service,由于这些Service都是单例,所以并不存在线程安全问题。 由于Controller本身是单例模式 (非线程安全的), 这意味着每个request过来,...

算法之名 ⋅ 今天 ⋅ 0

知乎Java数据结构

作者:匿名用户 链接:https://www.zhihu.com/question/35947829/answer/66113038 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 感觉知乎上嘲讽题主简...

颖伙虫 ⋅ 今天 ⋅ 0

Confluence 6 恢复一个站点有关使用站点导出为备份的说明

推荐使用生产备份策略。我们推荐你针对你的生产环境中使用的 Confluence 参考 Production Backup Strategy 页面中的内容进行备份和恢复(这个需要你备份你的数据库和 home 目录)。XML 导出备...

honeymose ⋅ 今天 ⋅ 0

JavaScript零基础入门——(九)JavaScript的函数

JavaScript零基础入门——(九)JavaScript的函数 欢迎回到我们的JavaScript零基础入门,上一节课我们了解了有关JS中数组的相关知识点,不知道大家有没有自己去敲一敲,消化一下?这一节课,...

JandenMa ⋅ 今天 ⋅ 0

火狐浏览器各版本下载及插件httprequest

各版本下载地址:http://ftp.mozilla.org/pub/mozilla.org//firefox/releases/ httprequest插件截至57版本可用

xiaoge2016 ⋅ 今天 ⋅ 0

Docker系列教程28-实战:使用Docker Compose运行ELK

原文:http://www.itmuch.com/docker/28-docker-compose-in-action-elk/,转载请说明出处。 ElasticSearch【存储】 Logtash【日志聚合器】 Kibana【界面】 答案: version: '2'services: ...

周立_ITMuch ⋅ 今天 ⋅ 0

使用快嘉sdkg极速搭建接口模拟系统

在具体项目研发过程中,一旦前后端双方约定好接口,前端和app同事就会希望后台同事可以尽快提供可供对接的接口方便调试,而对后台同事来说定好接口还仅是个开始、设计流程,实现业务逻辑,编...

fastjrun ⋅ 今天 ⋅ 0

PXE/KickStart 无人值守安装

导言 作为中小公司的运维,经常会遇到一些机械式的重复工作,例如:有时公司同时上线几十甚至上百台服务器,而且需要我们在短时间内完成系统安装。 常规的办法有什么? 光盘安装系统 ===> 一...

kangvcar ⋅ 昨天 ⋅ 0

使用Puppeteer撸一个爬虫

Puppeteer是什么 puppeteer是谷歌chrome团队官方开发的一个无界面(Headless)chrome工具。Chrome Headless将成为web应用自动化测试的行业标杆。所以我们很有必要来了解一下它。所谓的无头浏...

小草先森 ⋅ 昨天 ⋅ 0

Java Done Right

* 表示难度较大或理论性较强。 ** 表示难度更大或理论性更强。 【Java语言本身】 基础语法,面向对象,顺序编程,并发编程,网络编程,泛型,注解,lambda(Java8),module(Java9),var(...

风华神使 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部