我们大家都知道,无论是文字、图像还是声音,都必须以一定的格式来组织和存储起来,然后其它的软件再以相同的协议规则,相应的格式才能去打开解析这一段数据,例如,对于原始的图像数据,我们常见的格式有 YUV、Bitmap,而对于音频来说,最简单常见的格式就是 wav 格式了。今天我们简单的介绍一下如何简单的解析解析最简单的音频数据-wav文件。

在前面几篇主要介绍了如何利用android系统的 API 来完成原始音频信号的采集和播放,今天就讲解如何在 Android 平台上,将采集到的 PCM 音频数据保存到 wav 文件,同时,也介绍如何读取和解析 wav 文件。

在解析wav文件时,我们要先来看看wav文件的组成。

1.wav文件的组成


我们可以简单的分析一下这个 wav 格式头,它主要分为三个部分:

  1. 第一部分

    属于最“顶层”的信息块,通过“ChunkID”来表示这是一个 “RIFF”格式的文件,通过“Format”填入 “WAVE”来标识这是一个 wav 文件。而“ChunkSize”则记录了整个 wav 文件的字节数。

  2. 第二部分
    属于“fmt”信息块,主要记录了本 wav 音频文件的详细音频参数信息,例如:通道数、采样率、位宽等等

  3. 第三部分
    属于“data”信息块,由“Subchunk2Size”这个字段来记录后面存储的二进制原始音频数据的长度。

好了,分析到这里,我想大家应该就明白了,其实,做一种多媒体格式的解析,也不是一件特别复杂的事,说白了,格式就是一种规范,告诉你,我的二进制数据是怎么存储的,你只需要按照我的存储格的式来解析就可以了。

2.简单抽象的定义wav 文件头

我们可以定义一个如下的 Java 类来抽象和描述 wav 文件头:

package com.bnd.myaudioandvideo.wavclass WavFileHeader {var mChunkID = "RIFF"var mChunkSize = 0var mFormat = "WAVE"var mSubChunk1ID = "fmt "var mSubChunk1Size = 16var mAudioFormat: Short = 1var mNumChannel: Short = 1var mSampleRate = 8000var mByteRate = 0var mBlockAlign: Short = 0var mBitsPerSample: Short = 8var mSubChunk2ID = "data"var mSubChunk2Size = 0constructor() {}constructor(sampleRateInHz: Int, bitsPerSample: Int, channels: Int) {mSampleRate = sampleRateInHzmBitsPerSample = bitsPerSample.toShort()mNumChannel = channels.toShort()mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()}
}

下面我们就简单介绍一下这几个参数的意义,也可以参考:WAVE PCM声音文件格式
规范的WAVE格式以RIFF头开头:

参数名称 描述
ChunkID 包含ASCII格式的字母“ RIFF”(0x52494646大端格式)
ChunkSize 36 + SubChunk2Size,或更准确地说:4 +(8 + SubChunk1Size)+(8 + SubChunk2Size)这是其余块的大小 跟随这个数字。这是大小整个文件(以字节减去8个字节为单位) 此计数中未包括的两个字段: ChunkID和ChunkSize。
mFormat 包含字母“ WAVE”(0x57415645大端格式),“ WAVE”格式包含两个子块:“ fmt”和“ data”;“ fmt”子块描述了声音数据的格式
Subchunk1ID 包含字母“ fmt”(0x666d7420大端格式)
Subchunk1尺寸 用于PCM。这是大小, 该数字后面的Subchunk其余部
AudioFormat PCM = 1(即线性量化), 非1的值表示某些 压缩形式。
NumChannels Mono = 1,Stereo = 2,etc,
SampleRate 8000、44100等
ByteRate == SampleRate * NumChannels * BitsPerSample / 8
BlockAlign == NumChannels * BitsPerSample / 8; 一个样本的字节数,包括了所有频道。
BitsPerSample 8位= 8、16位= 16等
ExtraParamSize 如果是PCM,则不存在
ExtraParams 空间用于附加参数
Subchunk2ID 包含字母“ data”(0x64617461大端格式)。
Subchunk2Size == NumSamples * NumChannels * BitsPerSample / 8;这是数据中的字节数。您也可以将其视为大小在此之后的子块的读取 数字。
data 实际声音数据

例如,这是WAVE文件的开头72个字节,其中的字节显示为十六进制数字:

52 49 46 46 24 08 00 00 57 41 56 45 66 6d 74 20 10 00 00 00 01 00 02 00
22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 08 00 00 00 00 00 00
24 17 1e f3 3c 13 3c 14 16 f9 18 f9 34 e7 23 a6 3c f2 24 f2 11 ce 1a 0d

以下是这些字节作为WAVE声音文件的解释:

3.关于RIFF文件的一般讨论

多媒体应用程序需要存储和管理各种数据,包括位图,音频数据,视频数据和外围设备控制信息。RIFF提供了一种存储所有这些不同类型数据的方法。RIFF文件包含的数据类型由文件扩展名指示。可以存储在RIFF文件中的数据的示例是:

  1. 音频/视频交错数据(.AVI)
  2. 波形数据(.WAV)
  3. 位图数据(.RDI)
  4. MIDI信息(.RMI)
  5. 调色板(.PAL)
  6. 多媒体电影(.RMN)
  7. 动画光标(.ANI)
  8. 一捆其他RIFF文件(.BND)

注意:在这一点上,AVI文件是使用当前RIFF规范完全实现的唯一RIFF文件类型。尽管已经实现了WAV文件,但是这些文件非常简单,它们的开发人员通常在构建它们时会使用较旧的规范。

4.读取wav音频文件

自此,我们应该知道了wav 文件就是一段“文件头”+“音频二进制数据”。所以读写非常简单,如何读,如何写大致如下:

1. 写 wav 文件,其实就是先写入一个 wav 文件头,然后再继续写入音频二进制数据即可
2. 读 wav 文件,其实也就是先读一个 wav 文件头,然后再继续读出音频二进制数据即可

看了上面wav参数和RIFF的那么多介绍,我们在动手写代码之前,有两点是需要搞清楚:

  1. wav 文件头中,有哪些是“变化的”,哪些是“不变的”?

    比如:文件头开头的“RIFF”字符串就是“不变的”部分,而用来记录音频数据总长度的“Subchunk2Size”变量就是属于“变化的”部分,因为,再音频数据没有彻底全部写完之前,你是无法知道一共写入了多少字节的音频数据的,因此,这个部分,需要用一个变量记录起来,到全部写完之后,再使用 Java 的“RandomAccessFile”类,将文件指针跳转到“Subchunk2Size”字段,改写一下默认值即可。

  2. 如何把 int、short 变量与 byte[] 的转换
    因为 wav 文件都是二进制的方式读写,因此,“WavFileHeader”类中定义的变量都需要转换为byte字节流,具体转换方法如下:

 private fun intToByteArray(data: Int): ByteArray? {return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array()}private fun shortToByteArray(data: Short): ByteArray? {return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array()}private fun byteArrayToShort(b: ByteArray): Short {return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort()}private fun byteArrayToInt(b: ByteArray): Int {return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt()}

好了,最后附上完整的wav文件的读写代码如下:

  1. wav文件头的代码
package com.bnd.myaudioandvideo.wavimport java.nio.ByteBuffer
import java.nio.ByteOrderclass WavFileHeader {var mChunkID = "RIFF"var mChunkSize = 0var mFormat = "WAVE"var mSubChunk1ID = "fmt "var mSubChunk1Size = 16var mAudioFormat: Short = 1var mNumChannel: Short = 1var mSampleRate = 8000var mByteRate = 0var mBlockAlign: Short = 0var mBitsPerSample: Short = 8var mSubChunk2ID = "data"var mSubChunk2Size = 0constructor() {}constructor(sampleRateInHz: Int, bitsPerSample: Int, channels: Int) {mSampleRate = sampleRateInHzmBitsPerSample = bitsPerSample.toShort()mNumChannel = channels.toShort()mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()}private fun intToByteArray(data: Int): ByteArray? {return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array()}private fun shortToByteArray(data: Short): ByteArray? {return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array()}private fun byteArrayToShort(b: ByteArray): Short {return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort()}private fun byteArrayToInt(b: ByteArray): Int {return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt()}
}
  1. wav文件读取的代码
package com.bnd.myaudioandvideo.wavimport android.util.Log
import java.io.DataInputStream
import java.io.FileInputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrderclass WavFileReader {private var mDataInputStream: DataInputStream? = nullprivate var mWavFileHeader: WavFileHeader? = null@Throws(IOException::class)fun openFile(filepath: String?): Boolean {if (mDataInputStream != null) {closeFile()}mDataInputStream = DataInputStream(FileInputStream(filepath))return readHeader()}@Throws(IOException::class)fun closeFile() {if (mDataInputStream != null) {mDataInputStream!!.close()mDataInputStream = null}}fun getmWavFileHeader(): WavFileHeader? {return mWavFileHeader}fun readData(buffer: ByteArray?, offset: Int, count: Int): Int {if (mDataInputStream == null || mWavFileHeader == null) {return -1}try {val nbytes = mDataInputStream!!.read(buffer, offset, count)return if (nbytes == -1) {0} else nbytes} catch (e: IOException) {e.printStackTrace()}return -1}private fun readHeader(): Boolean {if (mDataInputStream == null) {return false}val header = WavFileHeader()val intValue = ByteArray(4)val shortValue = ByteArray(2)try {header.mChunkID = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()Log.d(TAG, "Read file chunkID:" + header.mChunkID)mDataInputStream!!.read(intValue)header.mChunkSize = byteArrayToInt(intValue)Log.d(TAG, "Read file chunkSize:" + header.mChunkSize)header.mFormat = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()Log.d(TAG, "Read file format:" + header.mFormat)header.mSubChunk1ID = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()Log.d(TAG, "Read fmt chunkID:" + header.mSubChunk1ID)mDataInputStream!!.read(intValue)header.mSubChunk1Size = byteArrayToInt(intValue)Log.d(TAG, "Read fmt chunkSize:" + header.mSubChunk1Size)mDataInputStream!!.read(shortValue)header.mAudioFormat = byteArrayToShort(shortValue)Log.d(TAG, "Read audioFormat:" + header.mAudioFormat)mDataInputStream!!.read(shortValue)header.mNumChannel = byteArrayToShort(shortValue)Log.d(TAG, "Read channel number:" + header.mNumChannel)mDataInputStream!!.read(intValue)header.mSampleRate = byteArrayToInt(intValue)Log.d(TAG, "Read samplerate:" + header.mSampleRate)mDataInputStream!!.read(intValue)header.mByteRate = byteArrayToInt(intValue)Log.d(TAG, "Read byterate:" + header.mByteRate)mDataInputStream!!.read(shortValue)header.mBlockAlign = byteArrayToShort(shortValue)Log.d(TAG, "Read blockalign:" + header.mBlockAlign)mDataInputStream!!.read(shortValue)header.mBitsPerSample = byteArrayToShort(shortValue)Log.d(TAG, "Read bitspersample:" + header.mBitsPerSample)header.mSubChunk2ID = "" + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar() + mDataInputStream!!.readByte().toChar()Log.d(TAG, "Read data chunkID:" + header.mSubChunk2ID)mDataInputStream!!.read(intValue)header.mSubChunk2Size = byteArrayToInt(intValue)Log.d(TAG, "Read data chunkSize:" + header.mSubChunk2Size)Log.d(TAG, "Read wav file success !")} catch (e: Exception) {e.printStackTrace()return false}mWavFileHeader = headerreturn true}companion object {private val TAG = WavFileReader::class.java.simpleNameprivate fun byteArrayToShort(b: ByteArray): Short {return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).short}private fun byteArrayToInt(b: ByteArray): Int {return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).int}}
}
  1. wav文件写的代码
package com.bnd.myaudioandvideo.wavimport java.io.*
import java.nio.ByteBuffer
import java.nio.ByteOrderclass WavFileWriter {private var mFilepath: String? = nullprivate var mDataSize = 0private var mDataOutputStream: DataOutputStream? = null@Throws(IOException::class)fun openFile(filepath: String?, sampleRateInHz: Int, channels: Int, bitsPerSample: Int): Boolean {if (mDataOutputStream != null) {closeFile()}mFilepath = filepathmDataSize = 0mDataOutputStream = DataOutputStream(FileOutputStream(filepath))return writeHeader(sampleRateInHz, bitsPerSample, channels)}@Throws(IOException::class)fun closeFile(): Boolean {var ret = trueif (mDataOutputStream != null) {ret = writeDataSize()mDataOutputStream!!.close()mDataOutputStream = null}return ret}fun writeData(buffer: ByteArray?, offset: Int, count: Int): Boolean {if (mDataOutputStream == null) {return false}mDataSize += try {mDataOutputStream!!.write(buffer, offset, count)count} catch (e: Exception) {e.printStackTrace()return false}return true}private fun writeHeader(sampleRateInHz: Int, channels: Int, bitsPerSample: Int): Boolean {if (mDataOutputStream == null) {return false}val header = WavFileHeader(sampleRateInHz, channels, bitsPerSample)try {mDataOutputStream!!.writeBytes(header.mChunkID)mDataOutputStream!!.write(intToByteArray(header.mChunkSize), 0, 4)mDataOutputStream!!.writeBytes(header.mFormat)mDataOutputStream!!.writeBytes(header.mSubChunk1ID)mDataOutputStream!!.write(intToByteArray(header.mSubChunk1Size), 0, 4)mDataOutputStream!!.write(shortToByteArray(header.mAudioFormat), 0, 2)mDataOutputStream!!.write(shortToByteArray(header.mNumChannel), 0, 2)mDataOutputStream!!.write(intToByteArray(header.mSampleRate), 0, 4)mDataOutputStream!!.write(intToByteArray(header.mByteRate), 0, 4)mDataOutputStream!!.write(shortToByteArray(header.mBlockAlign), 0, 2)mDataOutputStream!!.write(shortToByteArray(header.mBitsPerSample), 0, 2)mDataOutputStream!!.writeBytes(header.mSubChunk2ID)mDataOutputStream!!.write(intToByteArray(header.mSubChunk2Size), 0, 4)} catch (e: Exception) {e.printStackTrace()return false}return true}private fun writeDataSize(): Boolean {if (mDataOutputStream == null) {return false}try {val wavFile = RandomAccessFile(mFilepath, "rw")wavFile.seek(WavFileHeader.Companion.WAV_CHUNKSIZE_OFFSET.toLong())wavFile.write(intToByteArray(mDataSize + WavFileHeader.Companion.WAV_CHUNKSIZE_EXCLUDE_DATA), 0, 4)wavFile.seek(WavFileHeader.Companion.WAV_SUB_CHUNKSIZE2_OFFSET.toLong())wavFile.write(intToByteArray(mDataSize), 0, 4)wavFile.close()} catch (e: FileNotFoundException) {e.printStackTrace()return false} catch (e: IOException) {e.printStackTrace()return false}return true}companion object {private fun intToByteArray(data: Int): ByteArray {return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array()}private fun shortToByteArray(data: Short): ByteArray {return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array()}}
}

5.总结

  • WAVE数据文件假定的默认字节顺序为little-endian。使用big-endian字节排序方案写入的文件具有标识符RIFX而不是RIFF。
  • 样本数据必须以偶数字节边界结尾。不管它是什么意思。
  • 8位样本存储为0到255之间的无符号字节。16位样本存储为2的补码有符号整数,范围从-32768到32767。
  • Wave数据流中可能还有其他子块。如果是这样,则每个将具有char [4] SubChunkID,以及无符号长SubChunkSize和SubChunkSize的数据量。
  • RIFF代表 资源交换文件格式。

音频开发的知识点还是很多的,学习音频开发需要大家有足够的耐心,一步一个脚印的积累,只有这样才能把音频开发学好。下面推荐几个比较好的博主,希望对大家有所帮助。

  1. csdn博主:《雷神雷霄骅》
  2. 51CTO博客:《Jhuster的专栏》

Android音频开发(五)如何存储和解析最简单的音频wav文件相关推荐

  1. Android应用开发:数据存储和界面展现-1

    1. 相对布局RelativeLayout 特点:相对布局所有组件可以叠加在一起:各个组件的布局是独立的,互不影响:所有组件的默认位置都是在左上角(顶部.左部对齐) 属性 功能描述 android:l ...

  2. 【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )

    文章目录 一.分析 malloc 函数的 arm 汇编语言 一.分析 malloc 函数的 arm 汇编语言 在上一篇博客 [Android 逆向]arm 汇编 ( 使用 IDA 解析 arm 架构的 ...

  3. 【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 使用 IDA 打开 arm 动态库文件 | 切换 IDA 中汇编代码显示样式 )

    文章目录 一.使用 IDA 打开 arm 动态库文件 二.切换 IDA 中汇编代码显示样式 一.使用 IDA 打开 arm 动态库文件 分析 Android SDK 中的 arm 架构的动态库 , 动 ...

  4. 【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )

    文章目录 一.x86 汇编语言分析 一.x86 汇编语言分析 在上一篇博客 [Android 逆向]x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | 使用 IDA 打开动态库文件 | ...

  5. Android音频开发(3):如何播放一帧音频

    本文重点关注如何在Android平台上播放一帧音频数据.阅读本文之前,建议先读一下<Android音频开发(1):基础知识>,因为音频开发过程中,经常要涉及到这些基础知识,掌握了这些重要的 ...

  6. android数据的五种存储方式

    Android提供了5种方式存储数据 1 使用SharedPreferences存储数据 它的本质是基于XML文件存储key-value键值对数据,通常用来存储一些简单的配置信息. 其存储位置在/da ...

  7. Android音频开发(2):如何采集一帧音频

    本文重点关注如何在Android平台上采集一帧音频数据.阅读本文之前,建议先读一下我的上一篇文章<Android音频开发(1):基础知识>,因为音频开发过程中,经常要涉及到这些基础知识,掌 ...

  8. 安卓Android开发:使用AudioRecord录音、将录音保存为wav文件、使用AudioTrack保存录音

    一.使用AudioRrecord录音 1.1声明 首先需要声明一个AudioRecord类的实例.之所以需要事先声明,是因为在本例中,录音的启动和结束被封装在两个不同的方法里.而通常来讲," ...

  9. Android应用开发:数据存储和界面展现-2

    1. pull解析XML文件 Android推荐使用pull解析XML文件,与SAX解析XML文件类似,都是事件驱动类型的解析方式. 示例:获取天气信息 res\layout\activity_mai ...

最新文章

  1. XBOX ONE游戏开发之登陆服务器(一)
  2. 小菜学习Lucene.Net(更新3.0.3版本使用)
  3. 关于Unity中的刚体和碰撞器的相关用法(一)
  4. 禁用viewstate怎么还保存状态?
  5. 分布式搜索ElasticSearch单机与服务器环境搭建
  6. STL内嵌数据类型: value_type
  7. .net MVC Model
  8. 优秀案例|如何让网页首屏更具视觉吸引力?
  9. 复旦大学计算机学院官网,Computer and Information Science
  10. docker容易内部怎么编辑_在Docker的工作流中常见问题及最终方案
  11. C++的string类
  12. php 修改文件的权限_php修改文件权限
  13. 怎么用html打开图片,viewerjs 在html打开图片或打开pdf文件使用案例
  14. cad线性标注命令_CAD中线性标注的快捷命令是什么
  15. pyqt5和spyder版本对应_pyqt5 spyder 项目 记录
  16. 【高级篇 / SDWAN】(7.0) ❀ 03. SD-WAN 链路负载均衡的模式 ❀ FortiGate 防火墙
  17. BGP 十一条选路原则与BGP路由传递的注意事项介绍
  18. gel和react哪个厉害_gel、react、boost三种材料的跑鞋哪个更强呢?
  19. 开源软件之许可证(三)
  20. 图像迁移风格保存模型_一种图像风格迁移方法与流程

热门文章

  1. MFC开发IM-第三篇、资源视图--显示在另一个编辑器中打开
  2. 中国象棋源码c语言,中国象棋C语言源代码.doc
  3. Apple Music成为全球第二大音乐流媒体服务 远落后Spotify
  4. 上物理课还不够 张朝阳集结明星开启野雪挑战直播
  5. 贾跃亭:在性能、奢华和科技综合评比中 FF 91战胜了奔驰S迈巴赫、库里南
  6. 期权这块「饼」,互联网人吃不下去了
  7. OLED屏智能手机在出货量方面仍未占据主导地位 但预计今年将接近40%
  8. 任正非表示支持小女儿姚安娜搞文艺
  9. iPhone 12不标配充电器后,国产手机配件成了国外抢手货!
  10. 苹果确认部分AirPods Pro存在静电噪音等声音问题 将免费更换