背景
在学习投屏相关音视频开发时候,经常验证一些声音卡顿问题时候,需要对音频数据可能需要保存到本地,一般可能是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详细代码和资料参考如下链接
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