安卓开发:挑战每天发布一个封装类02--Wav录音封装类AudioChannel 1.0

news2024/10/7 15:31:13

简介

库名称:AudioChannel

版本:1.0

由于项目需求录音并base64编码存到服务器中,就顺手改装了一个别人的封装类

原封装类地址:Android AudioRecord音频录制wav文件输出 - 简书 (jianshu.com)

描述:此封装类基于AudioRecord实现wav的音频录制,本封装类对原版进行了以下修改:

1.部分修正

(1).可以看到,原封装类继承Thread,代码逻辑很清晰,因此改动过程也较轻松,单次运行能够正常,但是在二次运行,发现报错:

D/CompatibilityChangeReporter: Compat change id reported: 147798919; UID 10428; state: ENABLED
W/System.err: java.lang.IllegalThreadStateException
W/System.err:     at java.lang.Thread.start(Thread.java:960)
        at com.yy.audiochannaldemo.AudioChannel.startLive(AudioChannel.java:84)

经过跟踪发现,在二次运行的时候,线程的state变为TERMINATED,这意味着线程已经完成了它的执行并且已经退出。一旦线程终止,不能重新启动,因此新版封装类不再继承Thread,而是通过priavate线程重建函数initThread来实现。

(2).首先AudioRecord不能够直接保存录音为wav,因此必须先保存为pcm文件,再通过头部写入数据,转换为wav文件,在这个过程中注意到原封装库,没有对保存pcm的文件进行删除处理,后续可能导致容量过大

(3).构建函数,传入context,以此就无需动态授权外部存储写入权限,也方便后续需要context的操作部分

2.权限控制

在使用过程注意到,原版库并没有处理权限申请,在改版加上了,6.0以上安卓加入了权限控制,另外去除了使用外部存储,需要额外动态授权的情况,直接存入cache

3.功能实现

在原先基础上加入了音高、声音贝计算,并通过onResult接口回调这三个变量,不过db和hz都有一定偏差


一、配置部分

需要先在清单中加入这两项:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

此封装库需要配置的部分就这么多。

需要在build.gradle加入以下依赖添加代码

    implementation 'com.github.wendykierp:JTransforms:3.1'

二、代码部分

1.PcmToWavUtil.java:Pcm转Wav工具类

public class PcmToWavUtil {
    private int mBufferSize;  //缓存的音频大小
    private int mSampleRate = 8000;// 8000|16000
    private int mChannelConfig = AudioFormat.CHANNEL_IN_STEREO;   //立体声
    private int mChannelCount = 2;
    private int mEncoding = AudioFormat.ENCODING_PCM_16BIT;

    public PcmToWavUtil() {
        this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannelConfig, mEncoding);
    }

    public PcmToWavUtil(int sampleRate, int channelConfig, int channelCount, int encoding) {
        this.mSampleRate = sampleRate;
        this.mChannelConfig = channelConfig;
        this.mChannelCount = channelCount;
        this.mEncoding = encoding;
        this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannelConfig, mEncoding);
    }


    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = mSampleRate;
        int channels = mChannelCount;
        long byteRate = 16 * mSampleRate * channels / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;//44-8(RIFF+dadasize(4个字节))

            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加入wav文件头
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF/WAVE header
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';  //WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f'; // 'fmt ' chunk
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1;   // format = 1
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        header[32] = (byte) (2 * 16 / 8); // block align
        header[33] = 0;
        header[34] = 16;  // bits per sample
        header[35] = 0;
        header[36] = 'd'; //data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }
}

2.AudioChannel.java:录音封装类主体

package com.yy.audiochannaldemo;


import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;
import android.widget.Toast;

import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import org.jtransforms.fft.DoubleFFT_1D;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;


public class AudioChannel {

    private int sampleRate;
    private int channelConfig;
    private int minBufferSize;
    private byte[] buffer;
    private Thread recordThread;
    private AudioRecord audioRecord;
    private boolean isRecoding;
    private SimpleDateFormat sdf;
    String filename;
    Context context;
    long startTime;
    private onResult onResult;
    private DoubleFFT_1D fft;


    public AudioChannel(int sampleRate, int channels, Context context) {
        this.sampleRate = sampleRate;
        this.context = context;
        channelConfig = channels == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO;
        minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
        Log.i("AudioChannel", "minBufferSize: " + minBufferSize);
        buffer = new byte[minBufferSize];
        sdf = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
        fft = new DoubleFFT_1D(minBufferSize/ 2);
    }



    private double calculateRMS(byte[] audioBuffer) {
        double sum = 0.0;
        for (byte sample : audioBuffer) {
            sum += sample * sample;
        }
        return Math.sqrt(sum / audioBuffer.length);
    }

    // 将RMS值转换为分贝值的方法
    private double rmsToDB(double rms) {
        // 假设参考值为1(通常是最小可听声音的RMS值)
        double reference = 1.0;
        return 20 * Math.log10(rms / reference);
    }


    private double calculateHZ(byte[] buffer) {
        // Convert byte array to double array for FFT
        double[] fftBuffer = new double[buffer.length / 2];
        for (int i = 0; i < buffer.length; i += 2) {
            short sample = (short) ((buffer[i] << 8) | (buffer[i + 1] & 0xFF));
            fftBuffer[i / 2] = sample;
        }

        // 执行 FFT
        fft.realForward(fftBuffer);
        double maxAmplitude = 0.0;
        int pitchIndex = 0;
        for (int i = 0; i < fftBuffer.length-1; i++) {
            double amplitude = fftBuffer[i] * fftBuffer[i] + fftBuffer[i + 1] * fftBuffer[i + 1];
            if (i < fftBuffer.length / 2 && amplitude > maxAmplitude) {
                maxAmplitude = amplitude;
                pitchIndex = i;
            }
        }

        //计算hz,不过偏差比较大
        double frequency = (double) pitchIndex * sampleRate / (fftBuffer.length / 2) ;
        return frequency /100;

    }

    void initPremission() {
        ActivityCompat.requestPermissions((Activity)context, new String[]{Manifest.permission.RECORD_AUDIO}, 169);

    }


    void initThread() {
        this.recordThread=new Thread(){ //开线程
            @Override
            public void run() {
                audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
                audioRecord.startRecording();
                FileOutputStream writer = null;
                Date current = new Date();
                String time = sdf.format(current);
                byte[] audioBuffer = new byte[minBufferSize];  // 创建一个缓冲区
                try {
                    filename = context.getCacheDir() + "/" + time + ".pcm"; //cache目录不需要权限
                    writer = new FileOutputStream(filename, true);


                    while (!Thread.currentThread().isInterrupted() && isRecoding) { //如果线程没有Interrupted而且isRecording变量为True代表在录制状态的情况
                        if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                            audioRecord.read(audioBuffer, 0, minBufferSize); // 读取音频数据到缓冲区
                            double rms = calculateRMS(audioBuffer);
                            double db = rmsToDB(rms); //db的值
                            double hz = calculateHZ(audioBuffer);
                            int seaconds =(int) (System.currentTimeMillis() -startTime) /1000;
                            if (isRecoding)  {
                                ((Activity)context).runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        onResult.update(seaconds,db,hz); //如果还在线程运行状态把信息回调出来
                                    }
                                });


                            }
                            writer.write(audioBuffer);
                        }



                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    audioRecord.stop();
                    audioRecord.release();
                    audioRecord = null;
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                new PcmToWavUtil(44100,  AudioFormat.CHANNEL_IN_STEREO, 2, AudioFormat.ENCODING_PCM_16BIT).pcmToWav(filename, filename.replace("pcm","wav"));


            }
        };
    }








    public void startLive() { //录制
        initThread();
        initPremission();
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
                PackageManager.PERMISSION_GRANTED) {
            isRecoding = true;
            recordThread.start();
            startTime = System.currentTimeMillis();
         } else {
            Toast.makeText(context,"没有录音权限",Toast.LENGTH_LONG).show();
        }

    }

    public void stopLive(int mode) { //mode为-1时代表取消,为0代表取消
        if (!isRecoding) return;
        try {
            isRecoding = false;
            recordThread.join();
        } catch (Exception e){
            isRecoding = false;
            e.printStackTrace();
        }

        new File(filename).delete();
        switch (mode) {
            case 0:
                onResult.finish(filename.replace("pcm","wav")); //正常结束后pcm会被转换成wav
                break;
            case -1:
                new File(filename.replace("pcm","wav")).delete();
                onResult.cancel(); //取消回调
                break;
        }

    }







    public interface onResult { //三个回调
        void update(int seaconds,double db,double hz);
        void finish(String filename);
        void cancel();
    }
    public void onResult(onResult onResult) { //功能点击
        this.onResult = onResult;
    }



}

三.Demo部分

Demo下载地址:

gitee地址:

AudioChannel/demo · keyxh/AndroidUtils - 码云 - 开源中国 (gitee.com)

csdn地址:【免费】安卓开发:挑战每天发布一个封装类02-Wav录音封装类AudioChannel1.0资源资源-CSDN文库

在Demo中有两个Actvity

1.MainActvity:简易demo,示范调用

MainActvity的案例是普通调用,调用过程会将参数打印出来,结束时会将音频转base64,界面和logcat如下图所示,MainActvity的demo是没有任何交互

第二个demo:PPActvity(音高测试仪)

本来想做音高测试仪的,后来音高频率转换(例如440HZ转A4)没有整出来,后面有空再修改投放gitee,目前最终效果如下:

由福州职业技术学校温辉编写,欢迎搬运帮助更多人,但请带上以上这句。

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

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

相关文章

10.CSS3的calc函数

CSS3 的 calc 函数 经典真题 CSS 的计算属性知道吗&#xff1f; CSS3 中的 calc 函数 calc 是英文单词 calculate&#xff08;计算&#xff09;的缩写&#xff0c;是 CSS3 的一个新增的功能。 MDN 的解释为可以用在任何长度、数值、时间、角度、频率等处&#xff0c;语法如…

详细描述一下CrossOver2024版本的用途和作用?

当然可以。CrossOver 是一款由 CODE WEAVERS 公司开发的软件&#xff0c;其主要目标是在 macOS 和 Linux 系统上实现与 Windows 应用程序的兼容性。它不同于传统的虚拟机&#xff0c;如 Parallels 或 VMware&#xff0c;因为它并不在 macOS 上创建一个完整的 Windows 环境。相反…

【进程概念】

目录 什么是在计算机运行的程序这么多运行的程序计算机是如何管理的先描述再组织 什么是在计算机运行的程序 对于一个在磁盘可执行的二进制文件&#xff0c;也可叫做可执行程序。对于一个可执行的程序&#xff0c;程序有自己的代码和数据。一旦运行起来&#xff0c;就会在计算…

旅游分享系列之:福建旅游攻略

旅游分享系列之&#xff1a;福建旅游攻略 一、漳州1.福建土楼2.云水谣3.四菜一汤景点 二、厦门1.园林博览苑2.海上自行车道3.山海步道4.海滩5.闽南菜6.落日 三、泉州1.衙口沙滩2.海上日出3.珞珈寺4.海滩烟花 一、漳州 游玩2个景点&#xff1a;云水谣&#xff0c;四菜一汤可以住…

永恒之黑和永恒之蓝 漏洞解析及复现

充当攻击机的机器是kali&#xff0c;另外需要关闭防火墙策略。 永恒之黑&#xff08;CVE-2020-0796&#xff09; 靶机&#xff1a;windows 10的IP地址&#xff1a;192.168.200.30 原理&#xff1a;实现远程代码执行 注意&#xff1a; 这个脚本运行有时候会导致靶机蓝屏&am…

OpenAI Sora引领AI跳舞视频新浪潮:字节跳动发布创新舞蹈视频生成框架

OpenAI的Sora已经引起广泛关注&#xff0c;预计今年AI跳舞视频将在抖音平台上大放异彩。下面将为您详细介绍一款字节跳动发布的AI视频动画框架。 技术定位&#xff1a;这款框架采用先进的diffusion技术&#xff0c;专注于生成人类舞蹈视频。它不仅能够实现人体动作和表情的迁移…

(done) 什么是正定矩阵?Positive Definite Matrices

正定矩阵的定义&#xff1a;https://baike.baidu.com/item/%E6%AD%A3%E5%AE%9A%E7%9F%A9%E9%98%B5/11030459 正定矩阵的作用、验证视频&#xff1a;https://www.bilibili.com/video/BV1Ag411M76G/?spm_id_from333.337.search-card.all.click&vd_source7a1a0bc74158c6993c…

webpack配置杂记

1、热更新 安装webpack-dev-server : npm i webpack-dev-server -D webpack.config.js配置 module.exports {// 其他配置...,// 热更新配置devServer: {host: "localhost",port: 3000,}, } 2、入口entry&#xff1a;使用相对路径们也就是webpack程序运行的路径&am…

PNAS|多倍体物种极致突破

羊草&#xff08;Leymus chinensis&#xff09;是欧亚草原多年生优势草本植物&#xff0c;以其显著的适应性和饲料品质而闻名。尽管人们越来越认识到其生态和经济价值&#xff0c;但基因组序列的缺失及其遗传转化所面临的挑战限制了其在基础研究和野生植物改良方面的关键应用。…

transformer,视觉模型改进论文的讨论

1、efficientVIT efficientformer 模型 快48.9倍的新SAM&#xff01;清华&MIT&英伟达开源EfficientViT-SAM&#xff1a;精度不变&#xff0c;原地起飞 YOLOv8改进 | 2023主干篇 | EfficientViT替换Backbone&#xff08;高效的视觉变换网络&#xff09; - Snu77的文章 -…

《Python 语音转换简易速速上手小册》第5章 音频数据处理(2024 最新版)

文章目录 5.1 音频数据的基本处理5.1.1 基础知识5.1.2 主要案例&#xff1a;音频剪辑工具案例介绍案例 Demo案例分析 5.1.3 扩展案例 1&#xff1a;自动音量调节器案例介绍案例 Demo案例分析 5.1.4 扩展案例 2&#xff1a;语音识别预处理案例介绍案例 Demo案例分析 5.2 使用 Py…

dell r740服务器黄灯闪烁维修现场解决

1&#xff1a;首先看一下这款DELL非常主力的PowerEdge R740服务器长啥样&#xff0c;不得不说就外观来说自从IBM抛弃System X系列服务器后&#xff0c;也就戴尔这个外观看的比较顺眼。 图一&#xff1a;是DELL R740前视图&#xff08;这款是8盘机型&#xff09; 图二&#xff…

散列表Hash Table(哈希表)+散列函数+散列冲突及散列表插入、查找的时间复杂度分析

散列表(Hash Table)又名哈希表/Hash表,是根据键key直接访问在内存存储位置的值value的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性。但这个键key有时不是纯数值&#xff0c;不能够做为数组的下标&#xff0c;这时就可以把这个非纯数值的key转…

MediaPipe姿态识别pose_landmark_lite.tflite缺失问题

背景说明 最近朋友提供了一个姿态识别的简易代码&#xff0c;让自己帮忙调试改进一下。自己打开代码看了一下&#xff0c;使用的是mediapipe 框架进行的人体姿态检测&#xff0c;当我在配置好环境初始化pose&#xff08;姿态检测对象&#xff09;时出现了错误&#xff1a;Downl…

喀秋莎画中画怎么设置 喀秋莎画中画视频怎么导出 喀秋莎什么意思 camtasia studio下载

画中画视频&#xff0c;顾名思义&#xff0c;就是在一个视频中有两个画面&#xff0c;游戏解说、微课等类型的视频常常就以画中画的形式出现。作为一款专业的视频编辑软件&#xff0c;使用camtasia可以轻松地制作画中画视频并导出。接下来我将为大家介绍&#xff1a;喀秋莎画中…

Spring Boot中实现列表数据导出为Excel文件

点击下载《Spring Boot中实现列表数据导出为Excel文件》 1. 前言 本文将详细介绍在Spring Boot框架中如何将列表数据导出为Excel文件。我们将通过Apache POI库来实现这一功能&#xff0c;并解释其背后的原理、提供完整的流程和步骤&#xff0c;以及带有详细注释的代码示例。最…

bilibili尚硅谷周阳老师JUC并发编程与源码分析课程笔记第十一章——Synchronized与锁升级

文章目录 先从阿里及其它大厂面试题说起本章路线总纲阿里手册对锁使用的强制要求Synchronized锁优化的背景Synchronized锁的升级过程Synchronized锁的升级标志 Synchronized的性能变化Java5以前&#xff0c;只有Synchronized&#xff0c;这个是操作系统级别的重量级锁为什么每一…

推荐一款Vite中加载svg的小工具

最近开发中使用到一个好玩的Vite三方小插件vite-plugin-svg-icons很实用&#xff0c;可以辅助我们开发过程中快速加载svg小图标。其原理是在Vite编译器的时候通过一次性的DOM操作将SVG插入DOM结构中&#xff0c;然后通过使用内联SVG进行加载访问&#xff0c;极大的方便了我们的…

【码银送书第十二期】世界顶级名校计算机专业,都在用哪些书当教材?

清华、北大、MIT、CMU、斯坦福的学霸们在新学期里要学什么&#xff1f;今天我们来盘点一下那些世界名校计算机专业采用的教材。 01《深入理解计算机系统》 &#xff08;原书第3版&#xff09; 作者&#xff1a;兰德尔 E.布莱恩特 大卫 R. 奥哈拉伦 推荐理由&#xff1a;卡内基…

怿星科技测试实验室(EPT LABS)服务介绍

据中国汽车工业协会数据&#xff0c;2023年我国汽车产销量分别达3016.1万辆和3009.4万辆&#xff0c;年产销量双双创历史新高&#xff0c;汽车行业进入了新时代。新汽车时代下的OEM竞争更激烈&#xff0c;汽车电子架构更复杂&#xff0c;研发周期更短&#xff0c;软件迭代更快&…