需要源码请点赞关注收藏后评论区留下QQ~~~
语音通话功能要求实时传输,如果使用MediaRecorder与MediaPlayer组合,那么只能整句话都录完并编码好了才能传给对方去播放,这个时效性太差。
此时用到音频录制器AudioRecord与音轨播放器AudioTrack,该组合的音频格式为原始的二进制音频数据,没有文件头和文件尾,故而可以实现边录边播的实时语音对话
下面是AudioRecord的录音方法
getMinBufferSize 根据采样频率 声道配置音频格式获得合适的缓冲区大小
startRecording 开始录音
read 从缓冲区读取音频数据
stop 停止录音
setNotificationMarkerPosition 设置需要通知的标记位置
setRecordPositionUpdataListener 设置需要通知的时间周期
下面是AudioTrack的播音方法
setStereoVolume 设置立体声的音量
play 开始播音
write 把缓冲区的音频数据写入音轨
实战效果如下
可以在下拉框中选择频率 类型 编码格式等等
连接真机测试更佳
代码如下
package com.example.audio;
import android.media.AudioFormat;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.audio.task.AudioPlayTask;
import com.example.audio.task.AudioRecordTask;
import com.example.audio.util.DateUtil;
public class AudioRawActivity extends AppCompatActivity implements
OnCheckedChangeListener, AudioRecordTask.OnRecordListener, AudioPlayTask.OnPlayListener {
private static final String TAG = "AudioRawActivity";
private TextView tv_audio_record; // 声明一个文本视图对象
private CheckBox ck_audio_record; // 声明一个复选框对象
private TextView tv_audio_play; // 声明一个文本视图对象
private CheckBox ck_audio_play; // 声明一个复选框对象
private int mFrequence; // 音频的采样频率
private int mInChannel; // 音频的声道类型(录音时候)
private int mOutChannel; // 音频的声道类型(播音时候)
private int mFormat; // 音频的编码格式
private String mRecordFilePath; // 录制文件的保存路径
private AudioRecordTask mRecordTask; // 声明一个原始音频录制线程对象
private AudioPlayTask mPlayTask; // 声明一个原始音频播放线程对象
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_raw);
tv_audio_record = findViewById(R.id.tv_audio_record);
ck_audio_record = findViewById(R.id.ck_audio_record);
ck_audio_record.setOnCheckedChangeListener(this);
tv_audio_play = findViewById(R.id.tv_audio_play);
ck_audio_play = findViewById(R.id.ck_audio_play);
ck_audio_play.setOnCheckedChangeListener(this);
initFrequenceSpinner(); // 初始化采样频率的下拉框
initChannelSpinner(); // 初始化声道类型的下拉框
initFormatSpinner(); // 初始化编码格式的下拉框
}
// 初始化采样频率的下拉框
private void initFrequenceSpinner() {
ArrayAdapter frequenceAdapter = new ArrayAdapter<>(this,
R.layout.item_select, frequenceDescArray);
Spinner sp_frequence = findViewById(R.id.sp_frequence);
sp_frequence.setPrompt("请选择采样频率");
sp_frequence.setAdapter(frequenceAdapter);
sp_frequence.setOnItemSelectedListener(new FrequenceSelectedListener());
sp_frequence.setSelection(0);
}
private String[] frequenceDescArray = {"16000赫兹", "8000赫兹"};
private int[] frequenceArray = {16000, 8000};
class FrequenceSelectedListener implements AdapterView.OnItemSelectedListener {
public void onItemSelected(AdapterView> arg0, View arg1, int arg2, long arg3) {
mFrequence = frequenceArray[arg2];
}
public void onNothingSelected(AdapterView> arg0) {}
}
// 初始化声道类型的下拉框
private void initChannelSpinner() {
ArrayAdapter channelAdapter = new ArrayAdapter<>(this,
R.layout.item_select, channelDescArray);
Spinner sp_channel = findViewById(R.id.sp_channel);
sp_channel.setPrompt("请选择声道类型");
sp_channel.setAdapter(channelAdapter);
sp_channel.setSelection(0);
sp_channel.setOnItemSelectedListener(new ChannelSelectedListener());
}
private String[] channelDescArray = {"单声道", "立体声"};
private int[] inChannelArray = {AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_STEREO};
private int[] outChannelArray = {AudioFormat.CHANNEL_OUT_MONO, AudioFormat.CHANNEL_OUT_STEREO};
class ChannelSelectedListener implements AdapterView.OnItemSelectedListener {
public void onItemSelected(AdapterView> arg0, View arg1, int arg2, long arg3) {
mInChannel = inChannelArray[arg2];
mOutChannel = outChannelArray[arg2];
}
public void onNothingSelected(AdapterView> arg0) {}
}
// 初始化编码格式的下拉框
private void initFormatSpinner() {
ArrayAdapter formatAdapter = new ArrayAdapter<>(this,
R.layout.item_select, formatDescArray);
Spinner sp_format = findViewById(R.id.sp_format);
sp_format.setPrompt("请选择编码格式");
sp_format.setAdapter(formatAdapter);
sp_format.setSelection(0);
sp_format.setOnItemSelectedListener(new FormatSelectedListener());
}
private String[] formatDescArray = {"16位", "8位"};
private int[] formatArray = {AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_8BIT};
class FormatSelectedListener implements AdapterView.OnItemSelectedListener {
public void onItemSelected(AdapterView> arg0, View arg1, int arg2, long arg3) {
mFormat = formatArray[arg2];
}
public void onNothingSelected(AdapterView> arg0) {}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.getId() == R.id.ck_audio_record) {
if (isChecked) { // 开始录音
// 生成原始音频的文件路径
mRecordFilePath = String.format("%s/%s.pcm",
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
DateUtil.getNowDateTime());
ck_audio_record.setText("停止录音");
int[] params = new int[] {mFrequence, mInChannel, mFormat};
// 创建一个原始音频录制线程,并设置录制事件监听器
mRecordTask = new AudioRecordTask(this, mRecordFilePath, params, this);
mRecordTask.start(); // 启动原始音频录制线程
} else { // 停止录音
ck_audio_record.setText("开始录音");
mRecordTask.cancel(); // 原始音频录制线程取消录音
ck_audio_play.setVisibility(View.VISIBLE);
}
} else if (buttonView.getId() == R.id.ck_audio_play) {
if (isChecked) { // 开始播音
ck_audio_play.setText("暂停播音");
int[] params = new int[] {mFrequence, mOutChannel, mFormat};
// 创建一个原始音频播放线程,并设置播放事件监听器
mPlayTask = new AudioPlayTask(this, mRecordFilePath, params, this);
mPlayTask.start(); // 启动原始音频播放线程
} else { // 停止播音
ck_audio_play.setText("开始播音");
mPlayTask.cancel(); // 原始音频播放线程取消播音
}
}
}
// 在录音进度更新时触发
@Override
public void onRecordUpdate(int duration) {
String desc = String.format("已录制%d秒", duration);
tv_audio_record.setText(desc);
}
// 在录音完成时触发
@Override
public void onRecordFinish() {
ck_audio_record.setChecked(false);
Toast.makeText(this, "已结束录音,音频文件路径为"+mRecordFilePath, Toast.LENGTH_LONG).show();
}
// 在播音进度更新时触发
@Override
public void onPlayUpdate(int duration) {
String desc = String.format("已播放%d秒", duration);
tv_audio_play.setText(desc);
}
// 在播音完成时触发
@Override
public void onPlayFinish() {
ck_audio_play.setChecked(false);
Toast.makeText(this, "已结束播音", Toast.LENGTH_LONG).show();
}
}
原始的拖动条十分简陋,我们设计一个全新的控件来实现以下三点功能
1:显示音频的总时长
2:显示音频的已播放时长
3:提供暂停播放与恢复播放功能
完整的播控功能至少包含以下三项
1:关联音频路径与音频控制条
2:控制条实时显示当前播放进度
3:进度条的拖动操作实时传给媒体播放器
效果如下 可实现如下的自动播放暂停 拖动功能 而且更加美观
代码如下
package com.example.audio;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.audio.bean.AudioInfo;
import com.example.audio.util.MediaUtil;
import com.example.audio.widget.AudioController;
public class AudioControllerActivity extends AppCompatActivity {
private final static String TAG = "AudioControllerActivity";
private LinearLayout ll_controller; // 声明一个线性视图对象
private TextView tv_title; // 声明一个文本视图对象
private AudioController ac_play; // 声明一个音频控制条对象
private int CHOOSE_CODE = 3; // 只在音乐库挑选音频的请求码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_controller);
ll_controller = findViewById(R.id.ll_controller);
tv_title = findViewById(R.id.tv_title);
ac_play = findViewById(R.id.ac_play);
findViewById(R.id.btn_open).setOnClickListener(v -> {
// ACTION_GET_CONTENT只可选择近期的音频
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
// ACTION_PICK可选择所有音频
//Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("audio/*"); // 类型为音频
startActivityForResult(intent, CHOOSE_CODE); // 打开系统音频库
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (resultCode == RESULT_OK && requestCode == CHOOSE_CODE) { // 从音频库回来
if (intent.getData() != null) {
ll_controller.setVisibility(View.VISIBLE);
// 从content://media/external/audio/media/这样的Uri中获取音频信息
AudioInfo audio = MediaUtil.getPathFromContentUri(this, intent.getData());
ac_play.prepare(audio.getAudio()); // 准备播放指定路径的音频
ac_play.start(); // 开始播放
String desc = String.format("%s的《%s》", audio.getArtist(), audio.getTitle());
tv_title.setText("当前播放曲目名称:"+desc);
}
}
}
@Override
protected void onResume() {
super.onResume();
ac_play.resume(); // 恢复播放
}
@Override
protected void onPause() {
super.onPause();
ac_play.pause(); // 暂停播放
}
@Override
protected void onDestroy() {
super.onDestroy();
ac_play.release(); // 释放播放资源
}
}
创作不易 觉得有帮助请点赞关注收藏~~~