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

news2025/7/19 6:22:48

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/36049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]Python计算机毕业设计二手书交易系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

企业如何提供安全方面的投资回报率?

为什么增加在恢复方面的投资可以提高投资回报率? 所有企业都会认可安全的重要性&#xff0c;但在安全上的投入却经常令人迷惑。 一方面&#xff0c;由于安全威胁在不断变化&#xff0c;所以&#xff0c;安全建设维护需要长期持续大量投入。另一方面&#xff0c;长期大量投入后…

第6章 集成第3方依赖注入中间件“Autofac”

“Blog.Core-master”示例程序中接口及其具体实现类的注入操作&#xff0c;是通过第3方依赖注入中间件“Autofac”来以反射方式把Service.dll 程序集中所有接口及其具体实现类的实例依赖注入内置容器中。.Net(Core).x框架是中的内置依赖注入容器是不支持程序集注入的。 从最佳实…

葡萄糖-聚乙二醇-人血清白蛋白,HAS-PEG-Glucose,人血清白蛋白-PEG-葡萄糖

葡萄糖-聚乙二醇-人血清白蛋白,HAS-PEG-Glucose,人血清白蛋白-PEG-葡萄糖 人血清白蛋白(HSA)是一种高度水溶性的球状单体血浆蛋白&#xff0c;相对分子量为67KDa&#xff0c;由585个氨基酸残基、一个巯基和17个二硫键组成。在纳米颗粒载体中&#xff0c;HSA纳米颗粒与各种药物…

直播预告丨中高频多因子库存储的最佳实践

因子挖掘是量化交易的基础。随着历史交易数据日益增多&#xff0c;交易市场量化竞赛的不断升级和进化&#xff0c;量化投研团队开始面对数据频率高、因子数量多的场景&#xff0c;以10分钟线10000个因子5000个股票为例&#xff0c;一年的因子数据约为 2.3T 左右&#xff0c;1分…

BUUCTF Reverse/firmware

安装工具firmware-mod-kit &#xff0c;可以参考这个firmware-mod-kit工具安装和使用说明 最后make的时候报了个错&#xff0c;参考这个&#xff1a;linux安装firmware-mod-kit /firmware-mod-kit/src/uncramfs/uncramfs.c 加上头文件&#xff1a;#include <sys/sysmacros.h…

想做TikTok跨境电商?如何运营?

想做TikTok跨境电商&#xff1f;如何运营&#xff1f; 据官方报道&#xff1a; 2021年8月TikTok全球月活跃用户数已突破10亿 自2020年7月以来增长了45% 相比2020年初则增长了一倍 月活跃用户能达10亿&#xff0c;覆盖150个国家地区 你只要爆了一条视频 分一小杯羹就能舒…

13.5 GAS与连击

目录1. 连击检测窗口与砍出的第二刀1. 连击检测窗口与砍出的第二刀 连击的实现思路是&#xff0c;在劈砍动画的期间的某一段窗口期&#xff0c;令角色能够再次响应输入&#xff0c;完成伤害动作并进入新的连击动画&#xff0c;直到连招打完。所以核心关键就在于这一段连击窗口…

Halcon机器视觉实战--分水岭分割+距离变换实现粘连物体图像分割

分水岭的原理 把图像的灰度看作高度图,图像中每个像素点的灰度值看作该点的高度,高灰度值代表山脉,低灰度值代表盆地,每个局部最小值及其周围区域称为集水盆,而集水盆的边界则形成分水岭。 分水岭算法的步骤 1.彩色图像转化成单通道灰度图 2.求梯度图 3.在梯度图的基础…

BCH编码译码误码率性能matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法描述 BCH编译码是一种纠错能力强&#xff0c;构造简单的信道编译码。BCH编译码的生成多项式可以由如下的式子表示&#xff1a; ①BCH码是一种纠错码、线性分组码、循环码。 ②需要传输信息位…

如何搭建一个好的知识库管理系统?

简道云知识管理应用对于想知道怎样搭建一个好的知识库管理系统的人来说&#xff0c;拥有正确的流程非常重要。这个过程不需要很复杂&#xff0c;但如果您想要个满足更多需求的知识管理系统&#xff0c;它确实需要非常的全面。 简道云知识库&#xff1a;http://s.fanruan.com/r…

(附源码)计算机毕业设计JavaJava毕设项目社区物业管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven Vue 等等组成&#xff0c;B/…

软件测试面试,一定要准备的7个高频面试题(附答案,建议收藏)

收集了2022年最新的面试题后&#xff0c;负责就业的黑马讲师们整理出了7个高频出现的面试题&#xff0c;一起来看看。 高频问题1&#xff1a;请自我介绍下&#xff1f; 高频问题2&#xff1a;请介绍下最近做过的项目&#xff1f; 高频问题3&#xff1a;请介绍下你印象深刻的…

IO模型Netty

一、IO模型 对于一次IO操作&#xff0c;数据会先拷贝到内核空间中&#xff0c;然后再从内核空间拷贝到用户空间中&#xff0c;所以一次read操作&#xff0c;会经历以下两个阶段&#xff0c;基于这两个阶段就产生了五种不同的IO模式。 为了避免用户进程直接操作内核&#xff0c;…

【科学文献计量】RC.networkOneMode()中的参数解释

RC.networkOneMode中的参数解释 1 数据2 RC.networkOneMode()中的参数解释2.1 测试mode参数2.2 测试nodeCount参数2.3 测试edgeWeight参数2.4 测试stemmer参数2.5 测试edgeAttribute参数2.6 测试nodeAttribute参数1 数据 使用web of science中的数据,借助metaknowledge库读入…

跬智信息 (Kyligence) 荣获信创“大比武”重要奖项,坚持做大做实国产软件

近日&#xff0c;为期两个月的 2022 信创“大比武”活动圆满闭幕。经过层层筛选和考核&#xff0c;跬智信息 (Kyligence) 凭借“企业级智能多维数据分析解决方案”项目脱颖而出&#xff0c;在整体方案的技术架构、服务体系、安全架构、信创生态等方面得到了评委的高度认可&…

Python数学基础二、利用正弦sin求曲边图形的面积

目录 正弦 求曲边图形的面积 推导方式解法&#xff1a; 推导式解法&#xff1a; 正弦 古代的勾三股四弦五中说的弦就是我们要说的正弦&#xff0c;也就是直角三角形中的斜边&#xff0c;叫做弦&#xff0c;股就是人的大腿&#xff0c;古人称直角三角形长的那个直角边就叫做…

深入I/O挖矿

1、Linux 系统如何管理文件 1.1 静态文件与 inode 文件在没有被打开的情况下一般都是存放在磁盘中的&#xff0c;譬如电脑硬盘、移动硬盘、U 盘等外部存储设备&#xff0c;文件存放在磁盘文件系统中&#xff0c;并且以一种固定的形式进行存放&#xff0c;我们把他们称为静态文…

二、VSCode——MiKTeX编写latex编码

免安装下载VSCode https://blog.csdn.net/qq_40837795/article/details/128037675 下载MiKTeX https://miktex.org/download 配置MiKTeX https://blog.csdn.net/qq_40837795/article/details/120388489 配置VSCode LaTeX workshop 1、点击左侧Extensions&#xff0c;搜…

论文阅读笔记 | 三维目标检测——VoxelRCNN算法

如有错误&#xff0c;恳请指出。 文章目录1. 背景2. 网络结构Voxel QueryVoxel ROI PoolingAccelerated AggregationLoss Compute3. 实验结果paper&#xff1a;《Voxel R-CNN: Towards High Performance Voxel-based 3D Object Detection》 1. 背景 现有很多的point-based检测…