Android 音频开发 目录

  1. Android音频开发(1):音频相关知识
  2. Android音频开发(2):使用AudioRecord录制pcm格式音频
  3. Android音频开发(3):使用AudioRecord实现录音的暂停和恢复
  4. Android音频开发(4):PCM转WAV格式音频
  5. Android音频开发(5):Mp3的录制 - 编译Lame源码
  6. Android音频开发(6):Mp3的录制 - 使用Lame实时录制MP3格式音频
  7. Android音频开发(7):音乐可视化-FFT频谱图

项目地址:https://github.com/zhaolewei/MusicVisualizer
视频演示地址:https://www.bilibili.com/video/av30388154/

一、演示

二、实现

  • 实现流程:
  1. 使用MediaPlayer播放传入的音乐,并拿到mediaPlayerId
  2. 使用Visualizer类拿到拿到MediaPlayer播放中的音频数据(wave/fft)
  3. 将数据用自定义控件展现出来

三、准备工作

  • 使用Visualizer需要录音的动态权限, 如果播放sd卡音频需要STORAGE权限
      private static final String[] PERMISSIONS = new String[]{            Manifest.permission.RECORD_AUDIO,            Manifest.permission.MODIFY_AUDIO_SETTINGS      };            ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS, 1);          <uses-permission android:name="android.permission.RECORD_AUDIO" />    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

四、开始播放

private MediaPlayer.OnPreparedListener preparedListener = new     /**     * 播放音频     *     * @param raw 资源文件id     */    private  void doPlay(final int raw) {        try {            mediaPlayer = MediaPlayer.create(MyApp.getInstance(), raw);            if (mediaPlayer == null) {                Logger.e(TAG, "mediaPlayer is null");                return;            }            mediaPlayer.setOnErrorListener(errorListener);            mediaPlayer.setOnPreparedListener(preparedListener);        } catch (Exception e) {            Logger.e(e, TAG, e.getMessage());        }    }        /**    * 获取MediaPlayerId    * 可视化类Visualizer需要此参数    * @return  MediaPlayerId    */    public int getMediaPlayerId() {        return mediaPlayer.getAudioSessionId();    }

五、使用可视化类Visualizer获取当前音频数据

Visualizer 有两个比较重要的参数

  1. 设置可视化数据的数据大小 范围[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()[1]]
  2. 社会可视化数据的采集频率 范围[0~Visualizer.getMaxCaptureRate()]
  • OnDataCaptureListener 有2个回调,一个用于显示FFT数据,展示不同频率的振幅,另一个用于显示声音的波形图
private Visualizer.OnDataCaptureListener dataCaptureListener = new Visualizer.OnDataCaptureListener() {        @Override        public void onWaveFormDataCapture(Visualizer visualizer, final byte[] waveform, int samplingRate) {            audioView.post(new Runnable() {                @Override                public void run() {                    audioView.setWaveData(waveform);                }            });        }        @Override        public void onFftDataCapture(Visualizer visualizer, final byte[] fft, int samplingRate) {            audioView2.post(new Runnable() {                @Override                public void run() {                    audioView2.setWaveData(fft);                }            });        }    };    private void initVisualizer() {    try {        int mediaPlayerId = mediaPlayer.getMediaPlayerId();        if (visualizer != null) {            visualizer.release();        }        visualizer = new Visualizer(mediaPlayerId);                /**         *可视化数据的大小: getCaptureSizeRange()[0]为最小值,getCaptureSizeRange()[1]为最大值         */        int captureSize = Visualizer.getCaptureSizeRange()[1];        int captureRate = Visualizer.getMaxCaptureRate() * 3 / 4;                visualizer.setCaptureSize(captureSize);        visualizer.setDataCaptureListener(dataCaptureListener, captureRate, true, true);        visualizer.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);        visualizer.setEnabled(true);    } catch (Exception e) {        Logger.e(TAG, "请检查录音权限");    }}    

波形数据和傅里叶数据的关系请看面这张图:
Android音频开发(7):音乐可视化-FFT频谱图_第1张图片

快速傅里叶转换(FFT)详细分析: https://zhuanlan.zhihu.com/p/19763358

六、编写自定义控件,展示数据

  1. 处理数据: visualizer 回调中的数据中是存在负数的,需要转换一下,用于显示
  • 当byte 为 -128时Math.abs(fft[i]) 计算出来的值会越界,需要手动处理一下

byte 的范围: -128~127

    /**     * 预处理数据     *     * @return     */    private static byte[] readyData(byte[] fft) {        byte[] newData = new byte[LUMP_COUNT];        byte abs;        for (int i = 0; i < LUMP_COUNT; i++) {            abs = (byte) Math.abs(fft[i]);            //描述:Math.abs -128时越界            newData[i] = abs < 0 ? 127 : abs;        }        return newData;    }
  1. 紧接着就是根据数据去绘制图形
    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        wavePath.reset();        for (int i = 0; i < LUMP_COUNT; i++) {            if (waveData == null) {                canvas.drawRect((LUMP_WIDTH + LUMP_SPACE) * i,                        LUMP_MAX_HEIGHT - LUMP_MIN_HEIGHT,                        (LUMP_WIDTH + LUMP_SPACE) * i + LUMP_WIDTH,                        LUMP_MAX_HEIGHT,                        lumpPaint);                continue;            }            switch (upShowStyle) {                case STYLE_HOLLOW_LUMP:                    drawLump(canvas, i, false);                    break;                case STYLE_WAVE:                    drawWave(canvas, i, false);                    break;                default:                    break;            }            switch (downShowStyle) {                case STYLE_HOLLOW_LUMP:                    drawLump(canvas, i, true);                    break;                case STYLE_WAVE:                    drawWave(canvas, i, true);                    break;                default:                    break;            }        }    }        /**     * 绘制矩形条     */    private void drawLump(Canvas canvas, int i, boolean reversal) {        int minus = reversal ? -1 : 1;        if (waveData[i] < 0) {            Logger.w("waveData", "waveData[i] < 0 data: %s", waveData[i]);        }        float top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] * SCALE) * minus);        canvas.drawRect(LUMP_SIZE * i,                top,                LUMP_SIZE * i + LUMP_WIDTH,                LUMP_MAX_HEIGHT,                lumpPaint);    }    /**     * 绘制曲线     * 这里使用贝塞尔曲线来绘制     */    private void drawWave(Canvas canvas, int i, boolean reversal) {        if (pointList == null || pointList.size() < 2) {            return;        }        float ratio = SCALE * (reversal ? -1 : 1);        if (i < pointList.size() - 2) {            Point point = pointList.get(i);            Point nextPoint = pointList.get(i + 1);            int midX = (point.x + nextPoint.x) >> 1;            if (i == 0) {                wavePath.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio);            }            wavePath.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio,                    midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio,                    nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio);            canvas.drawPath(wavePath, lumpPaint);        }    }

更多相关文章

  1. Android音频开发(3):使用AudioRecord实现录音的暂停和恢复
  2. WPF核心的技术--数据绑定
  3. 让WebAPI 返回JSON格式的数据实例教程
  4. 怎么在EXECL文件中同时导入数据和插入图片?
  5. 关于.net C# Sql数据库SQLHelper类实例代码
  6. 什么是MVVM架构和数据绑定?
  7. 总结EF通用数据层封装类实例详解
  8. 总结MVC中数据验证实例
  9. 详解VS2015自带LocalDB数据库用法实例

随机推荐

  1. android mutex
  2. cocos2d-x for android容易忘记的地方记
  3. framework telephony
  4. Android新特性之TabLayout+ViewPager悬停
  5. Android(安卓)【插件化】"偷梁换柱"的高
  6. android 在adapter中更改Activity中的数
  7. Android(安卓)xml 解析
  8. Android中使用GridView分页显示系统所安
  9. 关于menu
  10. 在Android(安卓)2.3中如何使用native_act