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>

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

相关文章
|
5天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
8天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
6天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
19 5
|
5天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
6天前
|
缓存 数据库 Android开发
安卓开发中的性能优化技巧
【10月更文挑战第29天】在移动应用的海洋中,性能是船只能否破浪前行的关键。本文将深入探讨安卓开发中的性能优化策略,从代码层面到系统层面,揭示如何让应用运行得更快、更流畅。我们将以实际案例和最佳实践为灯塔,引领开发者避开性能瓶颈的暗礁。
18 3
|
9天前
|
存储 IDE 开发工具
探索Android开发之旅:从新手到专家
【10月更文挑战第26天】在这篇文章中,我们将一起踏上一段激动人心的旅程,探索如何在Android平台上从零开始,最终成为一名熟练的开发者。通过简单易懂的语言和实际代码示例,本文将引导你了解Android开发的基础知识、关键概念以及如何实现一个基本的应用程序。无论你是编程新手还是希望扩展你的技术栈,这篇文章都将为你提供价值和启发。让我们开始吧!
|
3天前
|
移动开发 Java Android开发
探索Android与iOS开发的差异性与互联性
【10月更文挑战第32天】在移动开发的大潮中,Android和iOS两大平台各领风骚。本文将深入浅出地探讨这两个平台的开发差异,并通过实际代码示例,展示如何在各自平台上实现相似的功能。我们将从开发环境、编程语言、用户界面设计、性能优化等多个角度进行对比分析,旨在为开发者提供跨平台开发的实用指南。
20 0
|
13天前
|
搜索推荐 Android开发 UED
安卓开发中的自定义视图:打造个性化用户界面
【10月更文挑战第22天】在安卓应用的海洋中,如何让你的应用脱颖而出?一个独特且直观的用户界面(UI)至关重要。本文将引导你通过自定义视图来打造个性化的用户体验,从基础的视图绘制到触摸事件的处理,我们将一步步深入探讨。准备好了吗?让我们开始吧!
|
1月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
1月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
99 1