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í)摆舟;
- PCM:音頻的原始數(shù)據(jù)(AudioFormat.ENCODING_PCM_16BIT、AudioFormat.ENCODING_PCM_8BIT邓了、AudioFormat.ENCODING_PCM_FLOAT等等)恨诱;不同的PCM代表不同的位深
- 采樣率:錄音設(shè)備在單位時(shí)間內(nèi)對(duì)模擬信號(hào)采樣的多少,采樣頻率越高驶悟,機(jī)械波的波形就越真實(shí)越自然胡野。常用的有16000(1.6KHz)、44100(44.1KHz)等
- 頻道:?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股冗、停止播放
停止播放是一套組合拳
- 停止播放
- 重置
- 釋放
//停止播放
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)管理