Android音频播放(本地/网络)绘制数据波形,根据特征有节奏的改变颜色

上一期刚刚掀完桌子没多久<a href="http://www.jianshu.com/p/2448e2903b07">《Android MP3录制,波形显示,音频权限兼容与播放》</a>,就有小伙伴问我:“一个音频的网络地址,如何根据这个获取它的波形图?”··· WTF(ノಠ益ಠ)ノ彡┻━┻,那一瞬间那是热泪盈眶啊,为什么我就没想到呢···反正肯定不是为了再水一篇文章就对了<( ̄︶ ̄)>。

</p>

<a href="https://github.com/CarGuo/RecordWave">我是DEMO,快点我点我</a>

</p>

改变颜色和播放输出波形

Android的音频播放与录制

MediaPlayer、MediaRecord、AudioRecord,这三个都是大家耳目能详的Android多媒体类(= =没听过的也要假装听过),包含了音视频播放,音视频录制等...但是还有一个被遗弃的熊孩子AudioTrack,这个因为太不好用了而被人过门而不入(反正肯定不是因为懒),这Android上多媒体四大家族就齐了,MediaPlayer、MediaRecord是封装好了的录制与播放,AudioRecord、AudioTrack是需要对数据和自定义有一定需要的时候用到的。(什么,还有SoundPool?我不听我不听...)

MP3的波形数据提取

当那位小伙提出这个需求的时候,我就想起了AudioTrack这个类,和AudioRecord功能的使用方法十分相似,使用的时候初始化好之后对数据的buffer执行write就可以发出呻吟了,因为数据是read出来的,所以你可以对音频数据做任何你爱做的事情。

但是问题来了,首先AudioTrack只能播放PCM的原始音频文件,那要MP3怎么办?这时候万能的Google告诉了我一个方向,"移植Libmad到android平台",类似上篇文章中利用mp3lame实现边录边转码的功能(有兴趣的朋友可以看一下,很不错)。

但WTF(ノಠ益ಠ)ノ彡┻━┻,这么重的模式怎么适合我们敏(lan)捷(ren)开发呢,调试JNI各种躺坑呢。这时候作为一个做责任的社会主义青少年,我发现了这个MP3RadioStreamPlayer,看简介:An MP3 online Stream player that uses MediaExtractor, MediaFormat, MediaCodec and AudioTrack meant as an alternative to using MediaPlayer....嗯~临表涕零,不知所言。

MediaCodec解码

4.1以上Android系统(这和支持所有系统有什么区别),支持mp3,wma等,可以用于编解码,感谢上帝,以前的自己真的孤陋顾问了。

其中MediaExtractor,我们需要支持网络数据,这个类可以负责中间的过程,即将从DataSource得到的原始数据解析成解码器需要的es数据,并通过MediaSource的接口输出。

下面直接看代码吧,都有注释(真的不是懒得讲╮(╯_╰)╭):

流程就是定义好buffer,初始化MediaExtractor来获取数据,MediaCodec对数据进行解码,初始化AudioTrack播放数据。

  • 因为上一期的波形播放数据是short形状的,所以我们为了兼容就把数据转为short,这里要注意合成short可能有大小位的问题,然后计算音量用于提取特征值。
ByteBuffer[] codecInputBuffers;
ByteBuffer[] codecOutputBuffers;

// 这里配置一个路径文件
extractor = new MediaExtractor();
try {
    extractor.setDataSource(this.mUrlString);
} catch (Exception e) {
    mDelegateHandler.onRadioPlayerError(MP3RadioStreamPlayer.this);
    return;
}

//获取多媒体文件信息
MediaFormat format = extractor.getTrackFormat(0);
//媒体类型
String mime = format.getString(MediaFormat.KEY_MIME);

// 检查是否为音频文件
if (!mime.startsWith("audio/")) {
    Log.e("MP3RadioStreamPlayer", "不是音频文件!");
    return;
}

// 声道个数:单声道或双声道
int channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
// if duration is 0, we are probably playing a live stream

//时长
duration = format.getLong(MediaFormat.KEY_DURATION);
// System.out.println("歌曲总时间秒:"+duration/1000000);

//时长
int bitrate = format.getInteger(MediaFormat.KEY_BIT_RATE);

// the actual decoder
try {
    // 实例化一个指定类型的解码器,提供数据输出
    codec = MediaCodec.createDecoderByType(mime);
} catch (IOException e) {
    e.printStackTrace();
}
codec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
codec.start();
// 用来存放目标文件的数据
codecInputBuffers = codec.getInputBuffers();
// 解码后的数据
codecOutputBuffers = codec.getOutputBuffers();

// get the sample rate to configure AudioTrack
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);

// 设置声道类型:AudioFormat.CHANNEL_OUT_MONO单声道,AudioFormat.CHANNEL_OUT_STEREO双声道
int channelConfiguration = channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
//Log.i(TAG, "channelConfiguration=" + channelConfiguration);

Log.i(LOG_TAG, "mime " + mime);
Log.i(LOG_TAG, "sampleRate " + sampleRate);

// create our AudioTrack instance
audioTrack = new AudioTrack(
        AudioManager.STREAM_MUSIC,
        sampleRate,
        channelConfiguration,
        AudioFormat.ENCODING_PCM_16BIT,
        AudioTrack.getMinBufferSize(
                sampleRate,
                channelConfiguration,
                AudioFormat.ENCODING_PCM_16BIT
        ),
        AudioTrack.MODE_STREAM
);

//开始play,等待write发出声音
audioTrack.play();
extractor.selectTrack(0);//选择读取音轨

// start decoding
final long kTimeOutUs = 10000;//超时
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

// 解码
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int noOutputCounter = 0;
int noOutputCounterLimit = 50;

while (!sawOutputEOS && noOutputCounter < noOutputCounterLimit && !doStop) {
    //Log.i(LOG_TAG, "loop ");
    noOutputCounter++;
    if (!sawInputEOS) {

        inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
        bufIndexCheck++;
        // Log.d(LOG_TAG, " bufIndexCheck " + bufIndexCheck);
        if (inputBufIndex >= 0) {
            ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];

            int sampleSize =
                    extractor.readSampleData(dstBuf, 0 /* offset */);

            long presentationTimeUs = 0;

            if (sampleSize < 0) {
                Log.d(LOG_TAG, "saw input EOS.");
                sawInputEOS = true;
                sampleSize = 0;
            } else {
                presentationTimeUs = extractor.getSampleTime();
            }
            // can throw illegal state exception (???)

            codec.queueInputBuffer(
                    inputBufIndex,
                    0 /* offset */,
                    sampleSize,
                    presentationTimeUs,
                    sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

            if (!sawInputEOS) {
                extractor.advance();
            }
        } else {
            Log.e(LOG_TAG, "inputBufIndex " + inputBufIndex);
        }
    }
    
    // decode to PCM and push it to the AudioTrack player
    // 解码数据为PCM
    int res = codec.dequeueOutputBuffer(info, kTimeOutUs);

    if (res >= 0) {
        //Log.d(LOG_TAG, "got frame, size " + info.size + "/" + info.presentationTimeUs);
        if (info.size > 0) {
            noOutputCounter = 0;
        }

        int outputBufIndex = res;
        ByteBuffer buf = codecOutputBuffers[outputBufIndex];

        final byte[] chunk = new byte[info.size];
        buf.get(chunk);
        buf.clear();
        if (chunk.length > 0) {
            //播放
            audioTrack.write(chunk, 0, chunk.length);

            //根据数据的大小为把byte合成short文件
            //然后计算音频数据的音量用于判断特征
            short[] music = (!isBigEnd()) ? byteArray2ShortArrayLittle(chunk, chunk.length / 2) :
                    byteArray2ShortArrayBig(chunk, chunk.length / 2);
            sendData(music, music.length);
            calculateRealVolume(music, music.length);

            if (this.mState != State.Playing) {
                mDelegateHandler.onRadioPlayerPlaybackStarted(MP3RadioStreamPlayer.this);
            }
            this.mState = State.Playing;
            hadPlay = true;
        }
        //释放
        codec.releaseOutputBuffer(outputBufIndex, false /* render */);
        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            Log.d(LOG_TAG, "saw output EOS.");
            sawOutputEOS = true;
        }
    } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
        codecOutputBuffers = codec.getOutputBuffers();

        Log.d(LOG_TAG, "output buffers have changed.");
    } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        MediaFormat oformat = codec.getOutputFormat();

        Log.d(LOG_TAG, "output format has changed to " + oformat);
    } else {
        Log.d(LOG_TAG, "dequeueOutputBuffer returned " + res);
    }
}

Log.d(LOG_TAG, "stopping...");

relaxResources(true);

this.mState = State.Stopped;
doStop = true;

// attempt reconnect
if (sawOutputEOS) {
    try {
        if (isLoop || !hadPlay) {
            MP3RadioStreamPlayer.this.play();
        }
        return;
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

显示波形和提取特征

既然都有数据了,那还愁什么波形,和上一期一样直接传┑( ̄Д  ̄)┍入AudioWaveView的List就好啦。

提取特征

这里曾经有过一个坑,躺尸好久,那时候的我还是个通信工程的孩纸,满脑子什么FFT快速傅里叶变化,求包络,自相关,卷积什么的,然后就从网上扒了一套算法很开心的计算频率和频谱,最后实现的效果很是堪忧,特别是录音条件下的实时效果很差,谁让我数学不是别人家的孩子呢┑( ̄Д  ̄)┍。

反正这次实现的没那么高深,很low的做法:

  • 先计算当前数据的音量大小(用上期MP3处理的方法)
  • 设置一个阈值
  • 判断阈值,与上一个数据比对
  • 符合就改变颜色
if (mBaseRecorder == null)
    return;

//获取音量大小
int volume = mBaseRecorder.getRealVolume();
//Log.e("volume ", "volume " + volume);

//缩减过滤掉小数据
int scale = (volume / 100);

//是否大于给定阈值
if (scale < 5) {
    mPreFFtCurrentFrequency = scale;
    return;
}

//这个数据和上个数据之间的比例
int fftScale = 0;
if (mPreFFtCurrentFrequency != 0) {
    fftScale = scale / mPreFFtCurrentFrequency;
}

//如果连续几个或者大了好多就可以改变颜色
if (mColorChangeFlag == 4 || fftScale > 10) {
    mColorChangeFlag = 0;
}

if (mColorChangeFlag == 0) {
    if (mColorPoint == 1) {
        mColorPoint = 2;
    } else if (mColorPoint == 2) {
        mColorPoint = 3;
    } else if (mColorPoint == 3) {
        mColorPoint = 1;
    }
    int color;
    if (mColorPoint == 1) {
        color = mColor1;
    } else if (mColorPoint == 2) {
        color = mColor3;
    } else {
        color = mColor2;
    }
    mPaint.setColor(color);
}
mColorChangeFlag++;
//保存数据
if (scale != 0)
    mPreFFtCurrentFrequency = scale;

...

/**
 * 此计算方法来自samsung开发范例
 *
 * @param buffer   buffer
 * @param readSize readSize
 */
protected void calculateRealVolume(short[] buffer, int readSize) {
    double sum = 0;
    for (int i = 0; i < readSize; i++) {
        // 这里没有做运算的优化,为了更加清晰的展示代码
        sum += buffer[i] * buffer[i];
    }
    if (readSize > 0) {
        double amplitude = sum / readSize;
        mVolume = (int) Math.sqrt(amplitude);
    }
}

怎么样,很简单是吧,有没感觉又被我水了一篇<( ̄︶ ̄)>,不知道你有没有收获呢,欢迎留言哟。

最后收两句:

有时候会听到有人说做业务代码只是在搬砖,对自己的技术没有什么提升,这种理论我个人并不是十分认同的,因为相对于自己开源和学习新的技术,业务代码可以让你更加严谨的对待你的代码,会遇到更多你无法回避的问题,各种各类的坑才是你提升的关键,当前,前提是你能把各种坑都保存好,不要每次都跳进去。所以,对你的工作好一些吧.....((/- -)/

个人Github : https://github.com/CarGuo

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

程序员的花样编程,你到底行不行?

【导读】:说到 C/C++ 代码技巧,也许会有童鞋说 ,这是属于 C/C++ 程序员离职前恶搞之类的抖机灵。即便想,也不能干。别忘了有这样一句编程名言:「在编写...

24650
来自专栏葬爱家族

Android高德之旅(11)绘制海量点废话海量总结

第7篇说过绘制一个Marker,如果我要绘制多个Marker呢,很简单调用多次就可以了。但是如果绘制的数量非常大呢?大到成千上万,甚至好几万,普通绘制Marke...

34930
来自专栏非著名程序员

移动开发之实现图片轮播效果

导语:前天发了一篇比较轻松愉快的文章,后台就有人发消息问我了,这是技术分享吗?我可以负责任的说,我们这个号不仅分享技术,也分享与程序员有关的幽默搞笑的文章,包括...

206100
来自专栏Debian社区

FFmpeg 3.4 发布,多媒体处理工具合集

FFmpeg 3.4 已发布。FFmpeg 是用于处理音频、视频、字幕和相关元数据的多媒体内容的库和工具的合集。

17230
来自专栏Nian糕的私人厨房

JavaScript 展开全文和收起全文

我们在浏览文章列表页的时候,往往只会看到一部分的摘要,在摘要下面会有一个展开全文的按钮,点开后就能看到完整内容,而原来的展开全文按钮此时变成了收齐全文的按钮,同...

15940
来自专栏LhWorld哥陪你聊算法

【自然语言处理篇】--Chatterbot聊天机器人

ChatterBot是一个基于机器学习的聊天机器人引擎,构建在python上,主要特点是可以自可以从已有的对话中进行学(jiyi)习(pipei)。

92550
来自专栏微信终端开发团队的专栏

微信 Android 视频编码爬过的那些坑

这篇文章主要将会对视频流的编码中两个常见问题进行分析:1. 视频编码器的选择(硬编 or 软编)?2. 如何对摄像头输出的YUV帧进行快速预处理(镜像,缩放,旋...

3.1K30
来自专栏Python与爬虫

[资源分享]计算机科学速成课

推荐 程序员的你一定要看,不是程序员的也可以看看,我已经安利刚中考完的我妹妹看了(培养程序媛...)

25030
来自专栏大大的微笑

观察者模式详解

观察者模式是众多软件设计模式中的一种,又称发布订阅模式.被观察的对象发生了变化,观察者对象们可以第一时间收到通知, 并作出相应的处理,例如作为一家餐馆的会员...

236100
来自专栏Crossin的编程教室

我在Python的艳阳里,大雪纷飞

你在南方的艳阳里大雪纷飞;我在北方的寒夜里四季如春。 昨天,南方不少地区,尤其是长江中下游一带迎来了一场难得的大雪。虽说给出行带来极大的不便,但也让我们这些没怎...

295110

扫码关注云+社区

领取腾讯云代金券