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

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 音频焦点管理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容