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", "寫(xiě)錄音數(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的值寫(xiě)死為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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容