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>

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

相关文章
|
2月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
2月前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
43 1
|
2天前
|
Dart 前端开发 Android开发
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
1月前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
65 19
|
2月前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
1月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
70 14
|
1月前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
1月前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
在数字时代,掌握安卓应用开发技能是进入IT行业的关键。本文将引导读者从零基础开始,逐步深入安卓开发的世界,通过实际案例和代码示例,展示如何构建自己的第一个安卓应用。我们将探讨基本概念、开发工具设置、用户界面设计、数据处理以及发布应用的全过程。无论你是编程新手还是有一定基础的开发者,这篇文章都将为你提供宝贵的知识和技能,帮助你在安卓开发的道路上迈出坚实的步伐。
39 5
|
1月前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
140 3
|
1月前
|
XML 搜索推荐 前端开发
安卓开发中的自定义视图:打造个性化UI组件
在安卓应用开发中,自定义视图是一种强大的工具,它允许开发者创造独一无二的用户界面元素,从而提升应用的外观和用户体验。本文将通过一个简单的自定义视图示例,引导你了解如何在安卓项目中实现自定义组件,并探讨其背后的技术原理。我们将从基础的View类讲起,逐步深入到绘图、事件处理以及性能优化等方面。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。

热门文章

最新文章