美文网首页Android开发Android 知识Android开发
Android音频播放(本地/网络)绘制数据波形,根据特征有节奏

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

作者: 恋猫月亮 | 来源:发表于2016-11-17 12:09 被阅读4131次
    上一期刚刚掀完桌子没多久<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

    看什么看

    相关文章

      网友评论

      • zzzmode:可以直接用android.media.audiofx.Visualizer 分析,不过需要权限:smile:
      • 8ff6bbf32ad6:大神,我已测试过,Android6.0的没法用,一直显示文件创建失败,经查阅资料,6.0好像不能在外部存储中创建文件夹,然后我把FileUntils那个工具类的目录改成了私有目录,文件就创建成功 了
        恋猫月亮:@小步子cc 哦哦,这个还真没注意,谢谢提醒!
      • 恋猫月亮:如果是想直接导入作为库使用,可以屏蔽lib下的gradle最底部的apply,然后对应把依赖文件从根目录的denpences.gradle拷贝到lib下。因为有时候会有同学反应直接导入后提示异常。
      • 卿辰:楼主知道怎么能把原唱去掉保留其他么。。。
        恋猫月亮:@卿辰 我就知道mkv格式的文件可以切换音轨实现,但是纯音频的不清楚,可能需要根据人声特征去清除吧
      • 恋猫月亮:有兴趣可以先看看,视频的 https://github.com/CarGuo/GSYVideoPlayer 后面如果有需要做个文章分享。

      本文标题:Android音频播放(本地/网络)绘制数据波形,根据特征有节奏

      本文链接:https://www.haomeiwen.com/subject/hsydpttx.html