投屏开发调试技能-pcm数据转wav格式文件源码实战分享

背景

在学习投屏相关音视频开发时候,经常验证一些声音卡顿问题时候,需要对音频数据可能需要保存到本地,一般可能是pcm格式的数据,但是pcm格式的数据是不可以用音乐播放器直接进行播放,需要专门的工具,而且你还需要知道pcm详细的具体参数,具体如下参数:
在这里插入图片描述
需要知道采样的位数格式,采样率,声道数目,字节顺序,因为只有知道这些参数播放器才知道怎么播放。

在这里插入图片描述
所以pcm播放还是比较麻烦,有需要考虑使用更加简单的文件格式,那就是下面要带大家进行手把手实战的wav格式。

Wav格式详细介绍

Wav简单介绍

WAV即波形声音文件格式 (Waveform Audio File Format,简称WAVE,因后缀为*.wav故简称WAV文件),其采用RIFF(Resource Interchange File Format,资源互换文件格式)结构,并符合(RIFF)规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持。Wave格式支持MSADPCM、CCITT A律、CCITT μ律和其他压缩算法,支持多种音频位数、采样频率和声道,是PC机上最为流行的声音文件格式;但由于“无损”的特点,WAV文件格式所占用的磁盘空间相对较大(每分钟的音乐大约需要12MB磁盘空间),故此文件格式多用于存储简短的声音片段。同时WAV文件格式通常用来保存PCM格式的原始音频数据,所以通常被称为无损音频(相对aac,mp3压缩格式来说,因为模拟到数字需要采样,无论如何都有失真)。但是严格意义上来讲,WAV也可以存储其它压缩格式的音频数据,但大部分都是pcm数据。

wav文件格式
pcm直接播放需要手动输入额外一些参数,wav格式就可以直接播放,就是因为wav有一个额外的文件头,文件头可以把这些参数进行放置,这样播放器就可以从wav文件头中获取pcm相关参数,实现直接播放wav的pcm数据
在这里插入图片描述
具体文件头格式如下表所示:
在这里插入图片描述
图中提到的RIFF 是 Resource Interchange File Format(资源交换文件格式)的简称。RIFF 是一种文件格式规范,用于在计算机系统之间交换和存储多媒体资源。WAV 文件格式是 Microsoft 的 RIFF 规范的一个子集。

格式说明总结:
上图可以看出来,wav文件格式都是由 chunk 组成,chunk 的格式如下:

在这里插入图片描述
在这里插入图片描述
里面了上面图后,再去写这个wav文件的head那么就变成非常简单了。
这里在重点介绍一下fmt部分的chunk数据,它们是pcm的格式参数的赋值部分

在这里插入图片描述

  • 音频格式(audio format):2个字节,表示音频数据的格式,具体可以对照下表,一般都是pcm就行
    在这里插入图片描述

  • 声道数(num channels):2个字节,表示音频数据的声道数。

  • 采样率(sample rate):4个字节,表示音频数据的采样率。

  • 每秒字节数(byte rate):4个字节,表示音频数据的数据速率。

  • 数据块对齐(block align):2个字节,表示数据块的对齐方式。

  • 位深度(bits per sample):2个字节,表示音频数据的位深度。

注意:同时注意左边字节顺序,一般字符都是大端模式,数字相关的都是小端模式,如上面的chunk名字都是大端一个个字符,其他数据大小都是小端。

编写代码实战

最重要要编写出一个wav头来

public static  byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
        long totalDataLen = pcmAudioByteCount + 36; // 不包含前8个字节的WAV文件总长度
        long byteRate = longSampleRate * 2 * channels;
        byte[] header = new byte[44];
         //RIFF Chunk
        header[0] = 'R'; // RIFF
        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';
        
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        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;
        //编码方式 10H为PCM编码格式
        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);
        //音频数据传送速率,采样率*通道数*采样深度/8
        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 * channels);
        header[33] = 0;
        //每个样本的数据位数
        header[34] = 16;
        header[35] = 0;
        
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (pcmAudioByteCount & 0xff);
        header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);
        header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);
        header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);
        return header;
    }

有了generateWavFileHeader这个方法后,针对固定大小的pcm转成wav文件已经完全可以搞定了,但是往往录音等pcm数据都是不断产生,pcm数据刚开始大小并不确定,所以这里可以采用种解决方法:
1、等完全录音完毕再把pcm写入到wav
2、因为wav的head一般是固定的大小44字节,这里可以先生成pcm大小size为0的head,这样可以站位44字节,等录制完成,重新生成head再覆盖原来head

在录音时候文件:

package com.example.remotesubmix;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Environment;
import android.util.Log;

import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class AudioRecordBussiness  extends Thread {
    private static final int AUDIO_RATE = 44100;
    static String PATH =null;
        private AudioRecord record;
        private int minBufferSize;
        private boolean isDone = false;

        public AudioRecordBussiness(Context context) {
            PATH = context.getExternalCacheDir().getAbsolutePath() ;
            /**
             * 获取最小 buffer 大小
             * 采样率为 44100,双声道,采样位数为 16bit
             */
            minBufferSize = AudioRecord.getMinBufferSize(AUDIO_RATE, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
            //使用 AudioRecord 去录音
            record = new AudioRecord(
                    MediaRecorder.AudioSource.REMOTE_SUBMIX,
                    AUDIO_RATE,
                    AudioFormat.CHANNEL_IN_STEREO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    minBufferSize
            );
        }

        @Override
        public void run() {
            super.run();
            FileOutputStream fos = null;
            FileOutputStream wavFos = null;
            RandomAccessFile wavRaf = null;
            try {
                //没有先创建文件夹
                File dir = new File(PATH);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                //创建 pcm 文件
                File pcmFile = getFile(PATH, "test.pcm");
                //创建 wav 文件
                File wavFile = getFile(PATH, "test.wav");
                fos = new FileOutputStream(pcmFile);
                wavFos = new FileOutputStream(wavFile);

                //先写头部,刚才是,我们并不知道 pcm 文件的大小
                byte[] headers = SaveToWaveFile.generateWavFileHeader(0, AUDIO_RATE, record.getChannelCount());
                wavFos.write(headers, 0, headers.length);

                //开始录制
                record.startRecording();
                byte[] buffer = new byte[minBufferSize];
                while (!isDone) {
                    //读取数据
                    int read = record.read(buffer, 0, buffer.length);
                    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                        //写 pcm 数据
                        fos.write(buffer, 0, read);
                        //写 wav 格式数据
                        wavFos.write(buffer, 0, read);
                    }

                }
                //录制结束
                record.stop();
                record.release();

                fos.flush();
                wavFos.flush();

                //修改头部的 pcm文件 大小
                wavRaf = new RandomAccessFile(wavFile, "rw");
                //pcmFile.length()只有pcm的数据大小,没有wav的head大小
                byte[] header = SaveToWaveFile.generateWavFileHeader(pcmFile.length(), AUDIO_RATE, record.getChannelCount());
                wavRaf.seek(0);
                wavRaf.write(header);

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                close(fos, wavFos,wavRaf);
            }
        }

        public void done() {
            isDone = true;
            interrupt();
        }
    private File getFile(String path, String name) {
        File file = new File(path, name);
        if (file.exists()) {
            file.delete();
        }
        try {
            file.createNewFile();
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void close(Closeable... closeables){
        if (closeables != null) {
            for (Closeable closeable : closeables) {
                if (closeable != null) {
                    try {
                        closeable.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

更多framework技术干货,请关注下面“千里马学框架”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

千里马学框架

帮助你了,就请我喝杯咖啡

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

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

打赏作者

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

抵扣说明:

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

余额充值