Android 开发中原始音频的录播和和自定义音频控制条的讲解及实战(超详细 附源码)
创始人
2024-02-15 18:45:50
0

需要源码请点赞关注收藏后评论区留下QQ~~~

一、原始音频的录播

语音通话功能要求实时传输,如果使用MediaRecorder与MediaPlayer组合,那么只能整句话都录完并编码好了才能传给对方去播放,这个时效性太差。

此时用到音频录制器AudioRecord与音轨播放器AudioTrack,该组合的音频格式为原始的二进制音频数据,没有文件头和文件尾,故而可以实现边录边播的实时语音对话

下面是AudioRecord的录音方法

getMinBufferSize 根据采样频率 声道配置音频格式获得合适的缓冲区大小

startRecording  开始录音

read 从缓冲区读取音频数据

stop 停止录音

setNotificationMarkerPosition 设置需要通知的标记位置

setRecordPositionUpdataListener 设置需要通知的时间周期

下面是AudioTrack的播音方法

setStereoVolume 设置立体声的音量

play 开始播音

write 把缓冲区的音频数据写入音轨 

实战效果如下

可以在下拉框中选择频率 类型 编码格式等等

连接真机测试更佳

 

代码如下

 Java类

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 implementsOnCheckedChangeListener, 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) {}}@Overridepublic 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(); // 原始音频播放线程取消播音}}}// 在录音进度更新时触发@Overridepublic void onRecordUpdate(int duration) {String desc = String.format("已录制%d秒", duration);tv_audio_record.setText(desc);}// 在录音完成时触发@Overridepublic void onRecordFinish() {ck_audio_record.setChecked(false);Toast.makeText(this, "已结束录音,音频文件路径为"+mRecordFilePath, Toast.LENGTH_LONG).show();}// 在播音进度更新时触发@Overridepublic void onPlayUpdate(int duration) {String desc = String.format("已播放%d秒", duration);tv_audio_play.setText(desc);}// 在播音完成时触发@Overridepublic void onPlayFinish() {ck_audio_play.setChecked(false);Toast.makeText(this, "已结束播音", Toast.LENGTH_LONG).show();}}

XML文件


二、自定义音频控制条

原始的拖动条十分简陋,我们设计一个全新的控件来实现以下三点功能

1:显示音频的总时长

2:显示音频的已播放时长

3:提供暂停播放与恢复播放功能

完整的播控功能至少包含以下三项

1:关联音频路径与音频控制条

2:控制条实时显示当前播放进度

3:进度条的拖动操作实时传给媒体播放器

效果如下 可实现如下的自动播放暂停 拖动功能 而且更加美观

 

 

 代码如下

Java类

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; // 只在音乐库挑选音频的请求码@Overrideprotected 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); // 打开系统音频库});}@Overrideprotected 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);}}}@Overrideprotected void onResume() {super.onResume();ac_play.resume(); // 恢复播放}@Overrideprotected void onPause() {super.onPause();ac_play.pause(); // 暂停播放}@Overrideprotected void onDestroy() {super.onDestroy();ac_play.release(); // 释放播放资源}}

XML文件


创作不易 觉得有帮助请点赞关注收藏~~~

相关内容

热门资讯

长春一女子打车遇“最炫出租车”... 极目新闻记者 王柳钦 近日,有网友发视频称,她和朋友在吉林长春乘坐出租车时,意外坐上了一辆改装有绚丽...
一次性信用修复政策落地,消金公... 一次性信用修复政策重磅落地,12月22日,中国人民银行发布一次性信用修复政策:明确对特定时期、特定金...
从“无”到“有”、从“有”到“... 浦东新区法律援助30周年暨《上海市法律援助若干规定》专题研讨会日前在浦东国际法律服务园举行。 本次研...
暴涨500%!19岁巴尔泰萨吉... 在意甲的舞台上,年轻球员的崛起往往令人瞩目,而19岁的巴尔泰萨吉正是近期最耀眼的新星之一。随着德转最...
完善幼儿园收费政策!三部门发文 来源 | 国家发展改革委微信,转自中国政府网微信 近日,国家发展改革委、教育部、财政部联合印发《关于...
老人“睡梦中”离世,警方靠共享... 孙大妈躺在床上 被子整整齐齐地盖在身上 就像睡着了一样 人却已经没有了呼吸 根据取款人的车辆骑行轨迹...
原创 中... 时值岁末,中航工业微信公众号中的说法,再一次引起了大家的注意。 近日,中航工业在“年末盘点”时,提...
“粤车南下”政策升级 今天起获... (央视财经《经济信息联播》)今天(23日)零时起,“粤车南下”驶入香港市区政策正式实施,广州、珠海、...
原创 李... 2025年12月23日零点,李艺彤在30岁生日当天,发文告别丝芭,强调"人生有梦,各自精彩"。丝芭1...
法律是否必然需要解释?新书《法... 本书聚焦法律解释的确定性或客观性问题——这是法治的一个重要预设,也是法哲学的一个核心难题。 为了回答...