Android音频播放绘制数据波形
ds576974
8年前
<p style="text-align:center"><img src="https://simg.open-open.com/show/b5fe25ace06f4074d09ebffe769adf1b.png"></p> <h3><strong>Android的音频播放与录制</strong></h3> <p>MediaPlayer、MediaRecord、AudioRecord,这三个都是大家耳目能详的Android多媒体类(= =没听过的也要假装听过),包含了音视频播放,音视频录制等...但是还有一个被遗弃的熊孩子 <strong>AudioTrack</strong> ,这个因为太不好用了而被人过门而不入(反正肯定不是因为懒),这Android上多媒体四大家族就齐了,MediaPlayer、MediaRecord是封装好了的录制与播放,AudioRecord、AudioTrack是需要对数据和自定义有一定需要的时候用到的。(什么,还有SoundPool?我不听我不听...)</p> <h3><strong>MP3的波形数据提取</strong></h3> <p>当那位小伙提出这个需求的时候,我就想起了AudioTrack这个类,和AudioRecord功能的使用方法十分相似,使用的时候初始化好之后对数据的buffer执行write就可以发出呻吟了,因为数据是read出来的,所以你可以对音频数据做任何你爱做的事情。</p> <p>但是问题来了,首先AudioTrack只能播放PCM的原始音频文件,那要MP3怎么办?这时候万能的Google告诉了我一个方向, "移植Libmad到android平台" ,类似上篇文章中利用 <em>mp3lame</em> 实现边录边转码的功能(有兴趣的朋友可以看一下,很不错)。</p> <p>但WTF(ノಠ益ಠ)ノ彡┻━┻,这么重的模式怎么适合我们敏(lan)捷(ren)开发呢,调试JNI各种躺坑呢。这时候作为一个做责任的社会主义青少年,我发现了这个MP3RadioStreamPlayer,看简介: <em>An MP3 online Stream player that uses MediaExtractor, MediaFormat, MediaCodec and AudioTrack meant as an alternative to using MediaPlayer.</em> ...嗯~临表涕零,不知所言。</p> <h3><strong>MediaCodec解码</strong></h3> <p>4.1以上Android系统(这和支持所有系统有什么区别),支持mp3,wma等,可以用于编解码,感谢上帝,以前的自己真的孤陋顾问了。</p> <p>其中 <strong>MediaExtractor</strong> ,我们需要支持网络数据,这个类可以负责中间的过程,即将从DataSource得到的原始数据解析成解码器需要的es数据,并通过MediaSource的接口输出。</p> <p>下面直接看代码吧,都有注释(真的不是懒得讲╮(╯_╰)╭):</p> <p>流程就是定义好buffer,初始化MediaExtractor来获取数据,MediaCodec对数据进行解码,初始化AudioTrack播放数据。</p> <ul> <li>因为上一期的波形播放数据是short形状的,所以我们为了兼容就把数据转为short,这里要注意合成short可能有大小位的问题,然后计算音量用于提取特征值。</li> </ul> <pre> 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(); } }</pre> <h3><strong>显示波形和提取特征</strong></h3> <p>既然都有数据了,那还愁什么波形,和上一期一样直接传┑( ̄Д  ̄)┍入 <strong>AudioWaveView</strong> 的List就好啦。</p> <p>提取特征</p> <p>这里曾经有过一个坑,躺尸好久,那时候的我还是个通信工程的孩纸,满脑子什么FFT快速傅里叶变化,求包络,自相关,卷积什么的,然后就从网上扒了一套算法很开心的计算频率和频谱,最后实现的效果很是堪忧,特别是录音条件下的实时效果很差,谁让我数学不是别人家的孩子呢┑( ̄Д  ̄)┍。</p> <p>反正这次实现的没那么高深,很low的做法:</p> <ul> <li>先计算当前数据的音量大小(用上期MP3处理的方法)</li> <li>设置一个阈值</li> <li>判断阈值,与上一个数据比对</li> <li>符合就改变颜色</li> </ul> <pre> 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); } }</pre> <p>怎么样,很简单是吧,有没感觉又被我水了一篇<( ̄︶ ̄)>,不知道你有没有收获呢,欢迎留言哟。</p> <p>最后收两句:</p> <p>有时候会听到有人说做业务代码只是在搬砖,对自己的技术没有什么提升,这种理论我个人并不是十分认同的,因为相对于自己开源和学习新的技术,业务代码可以让你更加严谨的对待你的代码,会遇到更多你无法回避的问题,各种各类的坑才是你提升的关键,当前,前提是你能把各种坑都保存好,不要每次都跳进去。所以,对你的工作好一些吧.....((/- -)/</p> <p> </p> <p> </p>