美文网首页
Android录制音频并使用ijkplayer播放

Android录制音频并使用ijkplayer播放

作者: TenXu | 来源:发表于2022-09-21 13:58 被阅读0次

    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、结束录制

    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;
            }
        }
    

    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);
    

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

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

    AudioRecorder开始录音方法

    recorder.startRecording();
    

    开启子线程,通过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();
            }
        }
    }
    

    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();
                }
            }
        }
    

    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;
        }
    }
    

    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 + "");
            }
        }
    }
    

    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;
        }
    

    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;
    
        }
    

    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'
    

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

    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'
    

    5.3、IjkMediaPlayer使用

    5.3.1、初始化

    IjkMediaPlayer player = new IjkMediaPlayer();
    

    5.3.2、配置播放源

    player.setDataSource(path);
    

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

    5.3.3、播放完成监听

    player.setOnCompletionListener(OnCompletionListener listener)
    

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

    5.3.4、准备监听

    player.setOnPreparedListener(OnPreparedListener listener)
    

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

    5.3.5、播放

    //准备
    player.prepareAsync();
    
    player.setOnPreparedListener(iMediaPlayer -> {
        iMediaPlayer.start();
    
    });
    

    5.3.6、停止播放

    停止播放是一套组合拳

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

    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;
        }
    }
    

    6.2、参考

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

    相关文章

      网友评论

          本文标题:Android录制音频并使用ijkplayer播放

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