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

简介

库名称: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,目前最终效果如下:

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

  • 30
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温辉wh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值