Android 开发中原始音频的录播和和自定义音频控制条的讲解及实战(超详细 附源码)

简介: Android 开发中原始音频的录播和和自定义音频控制条的讲解及实战(超详细 附源码)

需要源码请点赞关注收藏后评论区留下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 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<String> 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<String> 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<String> 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();
    }
}

XML文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:paddingLeft="5dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="采样频率:"
            android:textColor="@color/black"
            android:textSize="17sp" />
        <Spinner
            android:id="@+id/sp_frequence"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="left|center"
            android:spinnerMode="dialog" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:paddingLeft="5dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="声道类型:"
            android:textColor="@color/black"
            android:textSize="17sp" />
        <Spinner
            android:id="@+id/sp_channel"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="left|center"
            android:spinnerMode="dialog" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:paddingLeft="5dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="编码格式:"
            android:textColor="@color/black"
            android:textSize="17sp" />
        <Spinner
            android:id="@+id/sp_format"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="left|center"
            android:spinnerMode="dialog" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="5dp">
        <CheckBox
            android:id="@+id/ck_audio_record"
            style="@style/SwitchButton"
            android:layout_width="match_parent"
            android:checked="false"
            android:text="开始录音" />
        <TextView
            android:id="@+id/tv_audio_record"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:gravity="center"
            android:textColor="@color/black"
            android:textSize="17sp" />
        <CheckBox
            android:id="@+id/ck_audio_play"
            style="@style/SwitchButton"
            android:layout_width="match_parent"
            android:text="开始播音"
            android:textColor="@color/black"
            android:visibility="gone" />
        <TextView
            android:id="@+id/tv_audio_play"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:gravity="center"
            android:textColor="@color/black"
            android:textSize="17sp" />
    </LinearLayout>
</LinearLayout>

二、自定义音频控制条

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

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; // 只在音乐库挑选音频的请求码
    @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(); // 释放播放资源
    }
}

XML文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn_open"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="打开音频文件"
        android:textColor="@color/black"
        android:textSize="17sp" />
    <LinearLayout
        android:id="@+id/ll_controller"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:visibility="gone">
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dp"
            android:gravity="center"
            android:textColor="@color/black"
            android:textSize="17sp" />
        <com.example.audio.widget.AudioController
            android:id="@+id/ac_play"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="#cccccc" />
    </LinearLayout>
</LinearLayout>

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

相关文章
|
7月前
|
移动开发 前端开发 Android开发
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
1248 12
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
7月前
|
移动开发 JavaScript 应用服务中间件
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
956 5
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
7月前
|
移动开发 Android开发
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【03】建立隐私关于等相关页面和内容-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
344 0
|
前端开发 程序员 开发工具
|
7月前
|
移动开发 Rust JavaScript
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
1061 4
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
8月前
|
开发工具 Android开发
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
834 11
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
|
8月前
|
Java 开发工具 Maven
【01】完整的安卓二次商业实战-详细的初级步骤同步项目和gradle配置以及开发思路-优雅草伊凡
【01】完整的安卓二次商业实战-详细的初级步骤同步项目和gradle配置以及开发思路-优雅草伊凡
1007 6
|
10月前
|
安全 数据库 Android开发
在Android开发中实现两个Intent跳转及数据交换的方法
总结上述内容,在Android开发中,Intent不仅是活动跳转的桥梁,也是两个活动之间进行数据交换的媒介。运用Intent传递数据时需注意数据类型、传输大小限制以及安全性问题的处理,以确保应用的健壯性和安全性。
621 11
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
4208 77
|
10月前
|
移动开发 Java 编译器
Kotlin与Jetpack Compose:Android开发生态的演进与架构思考
本文从资深Android工程师视角深入分析Kotlin与Jetpack Compose在Android系统中的技术定位。Kotlin通过空安全、协程等特性解决了Java在移动开发中的痛点,成为Android官方首选语言。Jetpack Compose则引入声明式UI范式,通过重组机制实现高效UI更新。两者结合不仅提升开发效率,更为跨平台战略和现代架构模式提供技术基础,代表了Android开发生态的根本性演进。
408 0

热门文章

最新文章