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音频格式数据的相关知识;
- PCM:音频的原始数据(AudioFormat.ENCODING_PCM_16BIT、AudioFormat.ENCODING_PCM_8BIT、AudioFormat.ENCODING_PCM_FLOAT等等);不同的PCM代表不同的位深
- 采样率:录音设备在单位时间内对模拟信号采样的多少,采样频率越高,机械波的波形就越真实越自然。常用的有16000(1.6KHz)、44100(44.1KHz)等
- 频道:单声道输入频道、输出声道等,相关的值有(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、停止播放
停止播放是一套组合拳
- 停止播放
- 重置
- 释放
//停止播放
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 音频焦点管理