• Android录制音频并使用ijkplayer播放


    1、使用MediaRecorder录音

    1.1、开始录制

    private MediaRecorder mMediaRecorder;
    private File mTempFile;
    public void startRecordAudio(Context context) {
            
            //临时文件
            if (mTmpFile == null) {
                mTmpFile = SdcardUtils.getPublicFile(context, "record/voice.aac");
            }
    
            Log.i("tmpFile path", mTempFile.getPath());
            final File file = mTempFile;
            if (file.exists()) {
                file.delete();
            }
            MediaRecorder recorder = mMediaRecorder;
            if (recorder == null) {
                recorder = new MediaRecorder();
                mMediaRecorder = recorder;
                
                //设置输入源
                recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
                
                //设置音频输出格式/编码格式
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
                } else {
                    recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
                }
                recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
                
                //设置音频输出路径
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    recorder.setOutputFile(file);
                } else {
                    recorder.setOutputFile(file.getAbsolutePath());
                }
    
                try {
                    //准备录制
                    recorder.prepare();
    
                    //开始录制音频
                    recorder.start();
                    
                    requestAudioFocus();
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.e(TAG, e.toString());
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    1.2、结束录制

    public File stopRecordAudio() {
            final MediaRecorder recorder = mMediaRecorder;
            if (recorder != null) {
                try {
                    recorder.stop();
                    recorder.release();
                    mMediaRecorder = null;
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.e(TAG, e.toString());
                    return null;
                } finally {
                    abandonAudioFocus();
                }
            }
    
            File file = mTmpFile;
            if (file != null && file.exists() && file.length() > 0) {
                return file;
            } else {
                return null;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2、使用AudioRecorder录音

    在使用AudioRecorder时,需要了解采样率、频道配置和PCM音频格式数据的相关知识;

    1. PCM:音频的原始数据(AudioFormat.ENCODING_PCM_16BIT、AudioFormat.ENCODING_PCM_8BIT、AudioFormat.ENCODING_PCM_FLOAT等等);不同的PCM代表不同的位深
    2. 采样率:录音设备在单位时间内对模拟信号采样的多少,采样频率越高,机械波的波形就越真实越自然。常用的有16000(1.6KHz)、44100(44.1KHz)等
    3. 频道:单声道输入频道、输出声道等,相关的值有(AudioFormat.CHANNEL_IN_MONO,AudioFormat.CHANNEL_IN_STEREO等等)
    //根据采样率+音频格式+频道得到录音缓存大小
    int minBufferSize = AudioRecord.getMinBufferSize(16000,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
    • 1
    • 2
    • 3
    • 4

    针对AudioRecord的初始化,也需要采样率、PCM原始音频格式和频道,另外还需要录音缓存大小以及录音设备,如下:

    //MediaRecorder.AudioSource.MIC是麦克风录音设备,
    //minBufferSize是录音缓存大小
    new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
    
    • 1
    • 2
    • 3

    AudioRecorder开始录音方法

    recorder.startRecording();
    
    • 1

    开启子线程,通过read方法获取录音数据

    while (isRecording && !recordingAudioThread.isInterrupted()) {
        //获取录音数据
        read = mAudioRecorder.read(data, 0, data.length);
        if (AudioRecord.ERROR_INVALID_OPERATION != read) {
        try {
            fos.write(data);
            Log.i("audioRecord", "写录音数据->" + read);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.1、开始录制(完整代码)

    private AudioRecord mAudioRecorder;
    private File mTempFile;
    private boolean isRecording;
    private Thread recordingAudioThread;
    
    public void startRecordAudio(Context context) {
            //临时路径
            if (mTmpFile == null) {
                mTmpFile = SdcardUtils.getPublicFile(context, "record/voice.pcm");
            }
    
            Log.i("tmpFile path", mTmpFile.getPath());
            final File file = mTmpFile;
            if (file.exists()) {
                file.delete();
            }
    
            AudioRecord recorder = mAudioRecorder;
            if (recorder == null) {
                //16000是采样率,常用采样率有16000(1.6KHz),441000(44.1KHz)
                int minBufferSize = AudioRecord.getMinBufferSize(16000,
                        AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT);
    
                recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO,
                        AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
    
                mAudioRecorder = recorder;
    
                try {
                    //开始录制音频
                    isRecording = true;
                    recorder.startRecording();
    
                    recordingAudioThread = new Thread(() -> {
                        try {
                            file.createNewFile();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        FileOutputStream fos = null;
                        try {
                            fos = new FileOutputStream(file);
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                        if (fos != null) {
                            byte[] data = new byte[minBufferSize];
                            int read;
    
                            while (isRecording && !recordingAudioThread.isInterrupted()) {
                                read = mAudioRecorder.read(data, 0, data.length);
                                if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                                    try {
                                        fos.write(data);
                                        Log.i("audioRecord", "录音数据:" + read);
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }
                            }
    
                            try {
                                fos.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                    recordingAudioThread.start();
    
                    requestAudioFocus();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    2.2、结束录制

    public File stopRecordAudio() {
        isRecording = false;
    
        final AudioRecord audioRecord = mAudioRecorder;
        if (audioRecord != null) {
            audioRecord.stop();
            audioRecord.release();
            mAudioRecorder = null;
            recordingAudioThread.interrupt();
            recordingAudioThread = null;
        }
    
        File file = mTmpFile;
        if (file != null && file.exists() && file.length() > 0) {
            return file;
        } else {
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3、PCM格式转码AAC

    这个转码太难了,参考文章:Android pcm编码为aac
    不过该文章中的代码有bug,当采样率为44.1KHz的时候可以转AAC,并且正常播放,但当采样率为1.6KHz的时候,转成AAC之后播放的声音极为尖锐,调整了大半天后发现是addADTStoPacket方法中freqIdx的值写死为4了

    再参考了文章:Pcm 转 AAc,修复了该bug

    package com.example.recordvoice.utils;
    
    import android.media.AudioFormat;
    import android.media.AudioRecord;
    import android.media.MediaCodec;
    import android.media.MediaCodecInfo;
    import android.media.MediaFormat;
    import android.os.Build;
    import android.util.Log;
    
    import androidx.annotation.RequiresApi;
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class AacEncoder {
        ...
        private int sampleRateType;
    
    
        public void init(int sampleRate, int inChannel,
                         int channelCount, int sampleFormat,
                         String srcPath, String dstPath,
                         IHanlderCallback callback) {
    
            ...
            sampleRateType = ADTSUtils.getSampleRateType(mSampleRate);
            ...
        }
    
        ......
        ......
        ......
    
        private void addADTStoPacket(byte[] packet, int packetLen) {
            ....
            int freqIdx = sampleRateType;
            ....
        }
    
        static class ADTSUtils {
            private static Map<String, Integer> SAMPLE_RATE_TYPE;
    
            static {
                SAMPLE_RATE_TYPE = new HashMap<>();
                SAMPLE_RATE_TYPE.put("96000", 0);
                SAMPLE_RATE_TYPE.put("88200", 1);
                SAMPLE_RATE_TYPE.put("64000", 2);
                SAMPLE_RATE_TYPE.put("48000", 3);
                SAMPLE_RATE_TYPE.put("44100", 4);
                SAMPLE_RATE_TYPE.put("32000", 5);
                SAMPLE_RATE_TYPE.put("24000", 6);
                SAMPLE_RATE_TYPE.put("22050", 7);
                SAMPLE_RATE_TYPE.put("16000", 8);
                SAMPLE_RATE_TYPE.put("12000", 9);
                SAMPLE_RATE_TYPE.put("11025", 10);
                SAMPLE_RATE_TYPE.put("8000", 11);
                SAMPLE_RATE_TYPE.put("7350", 12);
            }
    
            public static int getSampleRateType(int sampleRate) {
                return SAMPLE_RATE_TYPE.get(sampleRate + "");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    4、音频焦点

    4.1、音频焦点意义

    当有两个或者两个以上音频同时向同一音频输出器播放,那么声音就会混在一起,为了避免所有音乐应用同时播放,就有了“音频焦点”的概念,希望做到 一次只能有一个应用获得音频焦点

    4.2、音频焦点获取

    private boolean mAudioFocus = false;
    private AudioFocusRequest mAudioFocusRequest;
    private AbsOnAudioFocusChangeListener mOnAudioFocusChangeListener;
    private android.media.AudioManager mAM;
    
        abstract static class AbsOnAudioFocusChangeListener implements android.media.AudioManager.OnAudioFocusChangeListener {
            boolean isEnabled = true;
    
            @Override
            public final void onAudioFocusChange(int focusChange) {
                if (isEnabled) {
                    onChane(focusChange);
                }
            }
    
            abstract void onChane(int focusChane);
    
        }
    
        private synchronized void requestAudioFocus() {
            android.media.AudioManager am = mAM;
    
            mOnAudioFocusChangeListener = new AbsOnAudioFocusChangeListener() {
                @Override
                void onChane(int focusChane) {
                    Log.i(TAG, "focusChane:" + focusChane);
    
                    synchronized (AudioManager.this) {
                        switch (focusChane) {
                            case AUDIOFOCUS_LOSS:
                            case AUDIOFOCUS_LOSS_TRANSIENT:
                            case AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                                if (mAudioFocus) {
                                    stopPlay(true, true);
                                } else {
                                    stopPlay(false, true);
                                }
                                break;
                            case AUDIOFOCUS_GAIN:
                                mAudioFocus = true;
                                break;
                        }
    
    
                    }
    
                }
    
            };
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                mAudioFocusRequest = new AudioFocusRequest.Builder(AUDIOFOCUS_GAIN)
                        .setOnAudioFocusChangeListener(mOnAudioFocusChangeListener).build();
                am.requestAudioFocus(mAudioFocusRequest);
            } else {
                am.requestAudioFocus(mOnAudioFocusChangeListener, AudioStream.MODE_NORMAL, AUDIOFOCUS_GAIN);
            }
    
            mAudioFocus = true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    4.3、放弃音频焦点

        private synchronized void abandonAudioFocus() {
            android.media.AudioManager am = mAM;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                if (mAudioFocusRequest != null) {
                    am.abandonAudioFocusRequest(mAudioFocusRequest);
                }
            } else {
                if (mOnAudioFocusChangeListener != null) {
                    am.abandonAudioFocus(mOnAudioFocusChangeListener);
                }
            }
    
            mAudioFocus = false;
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    5、IjkPlayer

    5.1、IjkPlayer简介

    IjkPlayer是BiliBili基于ffmpeg进行封装的一套视频播放器框架,所以ffmpeg支持的流媒体格式和视频格式ijk都是支持的;支持Android和IOS
    开源地址

    5.2、IjkPlayer引入

    # required, enough for most devices.
    # 常用
    implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
    
    # Other ABIs: optional
    # 其他cpu架构,现在Android上架必要有64位的架构,所以arm64现在已成为必须
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
    
    # ExoPlayer as IMediaPlayer: optional, experimental
    # Exo播放器,引入这个才可获得IjkMediaPlayer对象
    implementation 'tv.danmaku.ijk.media:ijkplayer-exo:0.8.8'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    根据实际情况,我的项目只需要引入如下:

    implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-exo:0.8.8'
    
    • 1
    • 2
    • 3

    5.3、IjkMediaPlayer使用

    5.3.1、初始化

    IjkMediaPlayer player = new IjkMediaPlayer();
    
    • 1

    5.3.2、配置播放源

    player.setDataSource(path);
    
    • 1

    path可以是本地的地址;也可以是在线音频地址,可用陈奕迅-孤勇者;也可直接播放rtmp流,找了很久没找到国内能播出来的电视台rtmp地址,最后用了这个:rtmp://media3.scctv.net/live/scctv_800

    5.3.3、播放完成监听

    player.setOnCompletionListener(OnCompletionListener listener)
    
    • 1

    不管播放成功与否,执行播放过程完成或视频播放完之后,就会回调完成方法

    5.3.4、准备监听

    player.setOnPreparedListener(OnPreparedListener listener)
    
    • 1

    在调用完成prepareAsync()之后,会回调该监听事件,但回调成功后,则可执行start方法播放

    5.3.5、播放

    //准备
    player.prepareAsync();
    
    player.setOnPreparedListener(iMediaPlayer -> {
        iMediaPlayer.start();
    
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5.3.6、停止播放

    停止播放是一套组合拳

    1. 停止播放
    2. 重置
    3. 释放
    //停止播放
    player.stop();
    //重置状态
    player.reset();
    //释放相关资源
    player.release();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6、补充

    6.1、SdcardUtils

    public class SdcardUtils {
        /**
         * 检查是否存在SD卡
         */
        public static boolean hasSdcard() {
            String state = Environment.getExternalStorageState();
            return state.equals(Environment.MEDIA_MOUNTED);
        }
    
        public static File getPublicFile(Context context, String child) {
            File file;
            if (hasSdcard()) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                    file = new File(Environment.getExternalStorageDirectory(), child);
                } else {
                    file = new File(context.getExternalFilesDir(Environment.DIRECTORY_MUSIC), child);
                }
            } else {
                file = new File(context.getFilesDir(), child);
            }
    
            mkdir(file.getParentFile());
    
            return file;
        }
    
        private static File mkdir(File dir) {
            if (!dir.exists()) {
                dir.mkdirs();
            }
            return dir;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    6.2、参考

    音频采样率
    安卓Android开发:使用AudioRecord录音、将录音保存为wav文件、使用AudioTrack保存录音
    音视频基础概念:PCM、采样率、位深和比特率
    Android pcm编码为aac
    Pcm 转 AAc
    Android 音频焦点管理

  • 相关阅读:
    编译GreatSQL with RocksDB引擎
    Linux——md5命令
    接物游戏demo
    【AI视野·今日Robot 机器人论文速览 第六十四期】Fri, 27 Oct 2023
    简约的博客网页制作 大学生个人博客网页设计模板 学生个人网页成品 DIV简单个人网站作品下载 静态HTML CSS个人网页作业源代码
    Ubuntu 12.04增加右键命令:在终端中打开增加打开文件
    Mysql语句中select、where、groupby、having。。顺序
    allatori8.0文档翻译-第七步:多个jar文件不混淆加水印
    虹科Pico汽车示波器 | 免拆诊断案例 | 2016款保时捷911 GT3 RS车发动机异响
    Apache Tomcat安装配置
  • 原文地址:https://blog.csdn.net/sinat_28884723/article/details/126977230