Android錄制音頻并使用ijkplayer播放

1潭袱、使用MediaRecorder錄音

1.1摇零、開(kāi)始錄制

private MediaRecorder mMediaRecorder;
private File mTempFile;
public void startRecordAudio(Context context) {
        
        //臨時(shí)文件
        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;
            
            //設(shè)置輸入源
            recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            
            //設(shè)置音頻輸出格式/編碼格式
            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);
            
            //設(shè)置音頻輸出路徑
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                recorder.setOutputFile(file);
            } else {
                recorder.setOutputFile(file.getAbsolutePath());
            }

            try {
                //準(zhǔn)備錄制
                recorder.prepare();

                //開(kāi)始錄制音頻
                recorder.start();
                
                requestAudioFocus();
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, e.toString());
            }
        }
    }

1.2磨确、結(jié)束錄制

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時(shí),需要了解采樣率乏奥、頻道配置和PCM音頻格式數(shù)據(jù)的相關(guān)知識(shí)摆舟;

  1. PCM:音頻的原始數(shù)據(jù)(AudioFormat.ENCODING_PCM_16BIT、AudioFormat.ENCODING_PCM_8BIT邓了、AudioFormat.ENCODING_PCM_FLOAT等等)恨诱;不同的PCM代表不同的位深
  2. 采樣率:錄音設(shè)備在單位時(shí)間內(nèi)對(duì)模擬信號(hào)采樣的多少,采樣頻率越高驶悟,機(jī)械波的波形就越真實(shí)越自然胡野。常用的有16000(1.6KHz)、44100(44.1KHz)等
  3. 頻道:?jiǎn)温暤垒斎腩l道痕鳍、輸出聲道等硫豆,相關(guān)的值有(AudioFormat.CHANNEL_IN_MONO龙巨,AudioFormat.CHANNEL_IN_STEREO等等)
//根據(jù)采樣率+音頻格式+頻道得到錄音緩存大小
int minBufferSize = AudioRecord.getMinBufferSize(16000,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT);

針對(duì)AudioRecord的初始化,也需要采樣率熊响、PCM原始音頻格式和頻道旨别,另外還需要錄音緩存大小以及錄音設(shè)備,如下:

//MediaRecorder.AudioSource.MIC是麥克風(fēng)錄音設(shè)備汗茄,
//minBufferSize是錄音緩存大小
new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);

AudioRecorder開(kāi)始錄音方法

recorder.startRecording();

開(kāi)啟子線程秸弛,通過(guò)read方法獲取錄音數(shù)據(jù)

while (isRecording && !recordingAudioThread.isInterrupted()) {
    //獲取錄音數(shù)據(jù)
    read = mAudioRecorder.read(data, 0, data.length);
    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
    try {
        fos.write(data);
        Log.i("audioRecord", "寫錄音數(shù)據(jù)->" + read);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.1、開(kāi)始錄制(完整代碼)

private AudioRecord mAudioRecorder;
private File mTempFile;
private boolean isRecording;
private Thread recordingAudioThread;

public void startRecordAudio(Context context) {
        //臨時(shí)路徑
        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 {
                //開(kāi)始錄制音頻
                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", "錄音數(shù)據(jù):" + read);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }

                        try {
                            fos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                recordingAudioThread.start();

                requestAudioFocus();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

2.2、結(jié)束錄制

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格式轉(zhuǎn)碼AAC

這個(gè)轉(zhuǎn)碼太難了绞铃,參考文章:Android pcm編碼為aac
不過(guò)該文章中的代碼有bug,當(dāng)采樣率為44.1KHz的時(shí)候可以轉(zhuǎn)AAC嫂侍,并且正常播放儿捧,但當(dāng)采樣率為1.6KHz的時(shí)候,轉(zhuǎn)成AAC之后播放的聲音極為尖銳挑宠,調(diào)整了大半天后發(fā)現(xiàn)是addADTStoPacket方法中freqIdx的值寫死為4了

再參考了文章:Pcm 轉(zhuǎn) AAc菲盾,修復(fù)了該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、音頻焦點(diǎn)

4.1各淀、音頻焦點(diǎn)意義

當(dāng)有兩個(gè)或者兩個(gè)以上音頻同時(shí)向同一音頻輸出器播放懒鉴,那么聲音就會(huì)混在一起,為了避免所有音樂(lè)應(yīng)用同時(shí)播放揪阿,就有了“音頻焦點(diǎn)”的概念疗我,希望做到 一次只能有一個(gè)應(yīng)用獲得音頻焦點(diǎn)

4.2、音頻焦點(diǎn)獲取

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南捂、放棄音頻焦點(diǎn)

    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簡(jiǎn)介

IjkPlayer是BiliBili基于ffmpeg進(jìn)行封裝的一套視頻播放器框架溺健,所以ffmpeg支持的流媒體格式和視頻格式ijk都是支持的麦牺;支持Android和IOS
開(kāi)源地址

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架構(gòu)鞭缭,現(xiàn)在Android上架必要有64位的架構(gòu)剖膳,所以arm64現(xiàn)在已成為必須
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播放器,引入這個(gè)才可獲得IjkMediaPlayer對(duì)象
implementation 'tv.danmaku.ijk.media:ijkplayer-exo:0.8.8'

根據(jù)實(shí)際情況岭辣,我的項(xiàng)目只需要引入如下:

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流墩瞳,找了很久沒(méi)找到國(guó)內(nèi)能播出來(lái)的電視臺(tái)rtmp地址驼壶,最后用了這個(gè):<font color="#0000dd">rtmp://media3.scctv.net/live/scctv_800</font>

5.3.3、播放完成監(jiān)聽(tīng)

player.setOnCompletionListener(OnCompletionListener listener)

不管播放成功與否喉酌,執(zhí)行播放過(guò)程完成或視頻播放完之后热凹,就會(huì)回調(diào)完成方法

5.3.4、準(zhǔn)備監(jiān)聽(tīng)

player.setOnPreparedListener(OnPreparedListener listener)

在調(diào)用完成prepareAsync()之后泪电,會(huì)回調(diào)該監(jiān)聽(tīng)事件般妙,但回調(diào)成功后,則可執(zhí)行start方法播放

5.3.5歪架、播放

//準(zhǔn)備
player.prepareAsync();

player.setOnPreparedListener(iMediaPlayer -> {
    iMediaPlayer.start();

});

5.3.6股冗、停止播放

停止播放是一套組合拳

  1. 停止播放
  2. 重置
  3. 釋放
//停止播放
player.stop();
//重置狀態(tài)
player.reset();
//釋放相關(guān)資源
player.release();

6、補(bǔ)充

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開(kāi)發(fā):使用AudioRecord錄音烹棉、將錄音保存為wav文件攒霹、使用AudioTrack保存錄音
音視頻基礎(chǔ)概念:PCM、采樣率浆洗、位深和比特率
Android pcm編碼為aac
Pcm 轉(zhuǎn) AAc
Android 音頻焦點(diǎn)管理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末催束,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子伏社,更是在濱河造成了極大的恐慌抠刺,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摘昌,死亡現(xiàn)場(chǎng)離奇詭異速妖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)聪黎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門罕容,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人稿饰,你說(shuō)我怎么就攤上這事锦秒。” “怎么了喉镰?”我有些...
    開(kāi)封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵旅择,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我侣姆,道長(zhǎng)生真,這世上最難降的妖魔是什么脖咐? 我笑而不...
    開(kāi)封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮汇歹,結(jié)果婚禮上屁擅,老公的妹妹穿的比我還像新娘。我一直安慰自己产弹,他們只是感情好派歌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著痰哨,像睡著了一般胶果。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斤斧,一...
    開(kāi)封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天早抠,我揣著相機(jī)與錄音,去河邊找鬼撬讽。 笑死蕊连,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的游昼。 我是一名探鬼主播甘苍,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼烘豌!你這毒婦竟也來(lái)了载庭?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤廊佩,失蹤者是張志新(化名)和其女友劉穎囚聚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體标锄,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顽铸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸯绿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跋破。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瓶蝴,靈堂內(nèi)的尸體忽然破棺而出毒返,到底是詐尸還是另有隱情,我是刑警寧澤舷手,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布拧簸,位于F島的核電站,受9級(jí)特大地震影響男窟,放射性物質(zhì)發(fā)生泄漏盆赤。R本人自食惡果不足惜贾富,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望牺六。 院中可真熱鬧颤枪,春花似錦、人聲如沸淑际。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)春缕。三九已至盗胀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锄贼,已是汗流浹背票灰。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宅荤,地道東北人屑迂。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像膘侮,于是被迫代替她去往敵國(guó)和親屈糊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容