欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > 投屏开发调试技能-pcm数据转wav格式文件源码实战分享

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

2024/10/25 15:19:48 来源:https://blog.csdn.net/learnframework/article/details/142133357  浏览:    关键词:投屏开发调试技能-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 Chunkheader[0] = 'R'; // RIFFheader[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';//WAVEheader[9] = 'A';header[10] = 'V';header[11] = 'E';//FMT Chunkheader[12] = 'f'; // 'fmt 'header[13] = 'm';header[14] = 't';header[15] = ' ';//过渡字节//数据大小header[16] = 16; // 4 bytes: size of 'fmt ' chunkheader[17] = 0;header[18] = 0;header[19] = 0;//编码方式 10H为PCM编码格式header[20] = 1; // format = 1header[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);//音频数据传送速率,采样率*通道数*采样深度/8header[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 chunkheader[36] = 'd';//dataheader[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);}@Overridepublic 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详细代码和资料参考如下链接

hal+perfetto+surfaceflinger

https://mp.weixin.qq.com/s/LbVLnu1udqExHVKxd74ILg
在这里插入图片描述

其他课程七件套专题:在这里插入图片描述
点击这里
https://mp.weixin.qq.com/s/Qv8zjgQ0CkalKmvi8tMGaw

视频试看:
https://www.bilibili.com/video/BV1wc41117L4/

参考相关链接:
https://blog.csdn.net/zhimokf/article/details/137958615

更多framework假威风耗:androidframework007

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com