无论是文字、图像还是声音,都必须以一定的格式来组织和存储起来,这样播放器才知道以怎样的方式去解析这一段数据,例如,对于原始的图像数据,我们常见的格式有 YUV、Bitmap,而对于音频来说,最简单常见的格式就是 wav 格式了。
wav 格式,与 bitmap 一样,都是微软开发的一种文件格式规范,它们都有一个相似之处,就是整个文件分为两部分,第一部分是“文件头”,记录重要的参数信息,对于音频而言,就包括:采样率、通道数、位宽等等,对于图像而言,就包括:图像的宽高、色彩位数等等;第二部分是“数据块”,即一帧一帧的二进制数据,对于音频而言,就是原始的 PCM 数据;对于图像而言,就是 RGB 数据。
前面几篇文章讲了如何利用 Android 平台的 API 完成原始音频信号的采集和播放,而本文则重点关注如何在 Android 平台上,将采集到的 PCM 音频数据保存到 wav 文件,同时,也介绍如何读取和解析 wav 文件。
而文章最后,我还会给出一段 AudioDemo 程序,该程序将最近的几篇文章涉及到的代码综合起来了,演示了一个完整的 Android 音频从采集到播放的全过程。
下面言归正传,讲讲如何读写 wav 文件格式。
1. 文件头
首先,我们了解一下 wav 格式的“文件头”
我们可以简单地分析一下这个 wav 格式头,它主要分为三个部分:
第一部分,属于最“顶层”的信息块,通过“ChunkID”来表示这是一个 “RIFF”格式的文件,通过“Format”填入“WAVE”来标识这是一个 wav 文件。而“ChunkSize”则记录了整个 wav 文件的字节数
第二部分,属于“fmt”信息块,主要记录了本 wav 音频文件的详细音频参数信息,例如:通道数、采样率、位宽等等
第三部分,属于“data”信息块,由“Subchunk2Size”这个字段来记录后面存储的二进制原始音频数据的长度。
分析到这里,我想大家应该就明白了,其实,做一种多媒体格式的解析,也不是一件特别复杂的事,说白了,格式就是一种规范,告诉你,我的二进制数据是怎么存储的,你应该按照什么样的方式来解析。
具体而言,我们可以定义一个如下的 Java 类来抽象和描述 wav 文件头:
/* * COPYRIGHT NOTICE * Copyright (C) 2016, Jhuster <lujun.hust@gmail.com> * https://github.com/Jhuster/AudioDemo * * @license under the Apache License, Version 2.0 * * @file WavFileHeader.java * * @version 1.0 * @author Jhuster * @date 2016/03/19 */package com.jhuster.audiodemo.api;public class WavFileHeader { public String mChunkID = "RIFF"; public int mChunkSize = 0; public String mFormat = "WAVE"; public String mSubChunk1ID = "fmt "; public int mSubChunk1Size = 16; public short mAudioFormat = 1; public short mNumChannel = 1; public int mSampleRate = 8000; public int mByteRate = 0; public short mBlockAlign = 0; public short mBitsPerSample = 8; public String mSubChunk2ID = "data"; public int mSubChunk2Size = 0; public WavFileHeader() { } public WavFileHeader(int sampleRateInHz, int bitsPerSample, int channels) { mSampleRate = sampleRateInHz; mBitsPerSample = (short)bitsPerSample; mNumChannel = (short)channels; mByteRate = mSampleRate*mNumChannel*mBitsPerSample/8; mBlockAlign = (short)(mNumChannel*mBitsPerSample/8); }}
具体每一个字段的含义,可以参考我上面给出的链接,下面我们再看看如何读写 wav 文件。
音视频开发学习地址:【免费】
FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发-学习视频教程-腾讯课堂
【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!~点击832218493加入(需要自取)
2. 读写 wav 文件
文章开头已经说过,其实说白了,wav 文件就是一段“文件头”+“音频二进制数据”,因此:
(1)写 wav 文件,其实就是先写入一个 wav 文件头,然后再继续写入音频二进制数据即可
(2)读 wav 文件,其实也就是先读一个 wav 文件头,然后再继续读出音频二进制数据即可
那么,在动手写代码之前,有两点你需要搞清楚:
(1) wav 文件头中,有哪些是“变化的”,哪些是“不变的”?
比如:文件头开头的“RIFF”字符串就是“不变的”部分,而用来记录音频数据总长度的“Subchunk2Size”变量就是属于“变化的”部分,因为,再音频数据没有彻底全部写完之前,你是无法知道一共写入了多少字节的音频数据的,因此,这个部分,需要用一个变量记录起来,到全部写完之后,再使用 Java 的“RandomAccessFile”类,将文件指针跳转到“Subchunk2Size”字段,改写一下默认值即可。
(2) 如何把 int、short 变量与 byte[] 的转换
因为 wav 文件都是二进制的方式读写,因此,“WavFileHeader”类中定义的变量都需要转换为byte字节流,具体转换方法如下:
private static byte[] intToByteArray(int data) { return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array();}private static byte[] shortToByteArray(short data) { return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array();} private static short byteArrayToShort(byte[] b) { return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort();} private static int byteArrayToInt(byte[] b) { return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt();}
关于 wav 文件读写的类我已经帮大家“封装”好了,并且结合着前面几篇文章给出的音频采集和播放的代码,完成了一个 AudioDemo 程序,放在我的 Github 上了,欢迎大家下载运行测试,然后结合着代码具体学习 Android 音频相关技术,代码地址:
https://github.com/Jhuster/AudioDemo
注:本系列文章的所有代码,以后都会并入到该 demo 项目中。