翻译 By Long Luo
原文链接:Android Audio: Play a WAV file on an AudioTrack
译者注: 1. 由于这是技术文章,所以有些词句使用原文,表达更准确。 2. 由于水平有效,有些地方可能翻译的不够准确,如有不当之处,敬请批评指正. 3. 针对某些语句,适当补充了上下文及更适合中文阅读,尽量做到信达雅。
如果你已经成功地了解了关于AudioTrack 的一些话题 ,那么你可能享受它带来的好处,例如低延迟(在STATIC(静态)模式),能够生成流式音频(在STREAM(流)模式)以及在播放之前,就能够访问和修改原始声音数据。
不过,现在的问题是如何从源获取数据。许多应用需要使用的AudioTrack 并不能简单的生成PCM音频(一个例子,比如Ethereal Dialpad 或者其他类似的App)。你可能需要从文件源去加载数据,例如WAV 或MP3 文件。
不要期望使用MediaPlayer ,去解码WAV 文件和MP3 音频。虽然MediaPlayer 播放这些文件非常好,但是其播放逻辑完全在Native层,同时并没有为我们提供额外选项,允许我们使用其他解码器实现我们的目的。因此,我们必须从手动地从音频文件进行解码出PCM 。
在这篇文章中,将会讨论WAV 格式文件。而在下一课中,我们将会更进一步,讨论如何从MP3 文件读取音频。
背景知识: 一些数字音频术语 如果你的App不是专门为数字音频设计,那么在继续我们的讨论之前,你可能需要先了解一些基本的缩略语。别担心,都很简单,我们不需要对此做深入挖掘。
PCM(脉冲调制方式 ) - 实现一个物理音频信号变成数字化最简单方法。基本原理就是信号变成了一个数字阵列,而其中每个数字代表的是声音在特定的时间瞬间的电平 也可以说是能量(振幅)。(如果这种解释在科学上可能不会很准确,那我就只能说声抱歉了)。信不信由你,你可以使用这种方法表示任何复杂 的声音,而且回放出来也非常精准。在这里,我们将只会谈到线性PCM。在线性PCM中,其中阵列中的每个数字都是原始声音振幅的线性表示 。在某些情况下,对数映射能够更好地表示原来的声音幅度比例情况 - 但是我们不会讨论那些情况。
Sampling rate(采样率):- 每秒你的数字声音有多少样本(声音幅度用数字表示)。样本越多,你能得到声音质量越好。目前在消费类音频系统目前使用的采样率通常是22050,44100和48000Hz/s。
每个样品分辨率/采样大小/位 - 定义表示振幅数字的大小和格式。例如,如果您使用的是8位整数,你只能表达出256级的幅度,所以原来的物理波形将被简化为256个离散电平,与此同时,你将失去一些声音精度也可以说是质量。如果你使用16位,那么声音质量变得更好。事实上,大部分时间你可能会使用16位音频。其他选项包括24位,32位(这些都是Android现在不支持的),或是使用浮点数。
声道 - 既可以是单声道,也可以是立体声(2个声道),或者更多声道(但是Android不支持)。如果你想要有立体声,你需要有立体声音频,就必须要在每个声道都需要有一个独立的PCM数组,相应的信息量也会翻倍。
上述定义也有助于你理解特定的格式和长度的音频缓冲区的数据量,以便提前预备缓冲区。也就是你需要一个缓冲区,以用于存储5秒长度以44100Hz采样率的立体声16-bit线性PCM数据。数据计算公式如下所示:
5 sec * 44100 samples per sec * 2 bytes per sample * 2 channels = 882,000 bytes
这一数额所需的内存可能会让初学者感到惊讶,因为当你往你的磁盘上存储的音频时,一个MP3文件,一个880KB的文件就可以容纳以相同的采样率和分辨率1分钟时长的音轨。这是为什么呢?因为先进的格式,比如MP3格式。因为我们大脑无法分辨识别出一些音频的内容,所以使用了很多复杂的方式在压缩的过程中去掉了这些内容。然而,大多数低等级的音频API,包括Android的AudioTrack 只能接受线性PCM 。这就是为什么如果我们不能把整个样品都放在内存中,我们需要将要处理的数据流,循环缓冲区和其他聪明的方式来使用音频API。
希望这样的解释并没有让你产生困惑,现在让我们继续来实际做一些与Android上的数字音频有关的工作吧!
WAV文件格式 我们的目标是用一个InputStream ,由其从一个WAV 文件加载PCM数据,来提供原始字节数据。然后我们就可以将原始的PCM数据直接推送到使用已经正确的配置好了的AudioTrack.write ,通过使用AudioTrack.write() 这个API。
WAV 文件包含一个文件头和具体数据会。我们需要读取文件头以知道诸如采样速率,分辨率等信息。另外,我们通过文件头,也可以知道此格式是否支持。WAV 可以封装成多种格式,我们无法全部支持。也许,只是合理的采样率,分辨率和通道的线性PCM 格式。
WAV 格式的细节在互联网上都可以找到,你仅仅需要在Google上搜索下。但是,遗憾的是,我并没有搜索到一个很好的Java库来读取WAV文件,而且可以移植到Android 下。因此,我自己写了一些简单的代码。
下面这个方法就是如何读取一个WAV文件的头部:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private static final String RIFF_HEADER = "RIFF" ;private static final String WAVE_HEADER = "WAVE" ;private static final String FMT_HEADER = "fmt " ;private static final String DATA_HEADER = "data" ;private static final int HEADER_SIZE = 44 ;private static final String CHARSET = "ASCII" ;public static WavInfo readHeader (InputStream wavStream) throws IOException, DecoderException { ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE); buffer.order(ByteOrder.LITTLE_ENDIAN); wavStream.read(buffer.array(), buffer.arrayOffset(), buffer.capacity()); buffer.rewind(); buffer.position(buffer.position() + 20 ); int format = buffer.getShort(); checkFormat(format == 1 , "Unsupported encoding: " + format); int channels = buffer.getShort(); checkFormat(channels == 1 || channels == 2 , "Unsupported channels: " + channels); int rate = buffer.getInt(); checkFormat(rate <= 48000 && rate >= 11025 , "Unsupported rate: " + rate); buffer.position(buffer.position() + 6 ); int bits = buffer.getShort(); checkFormat(bits == 16 , "Unsupported bits: " + bits); int dataSize = 0 ; while (buffer.getInt() != 0x61746164 ) { Log.d(TAG, "Skipping non-data chunk" ); int size = buffer.getInt(); wavStream.skip(size); buffer.rewind(); wavStream.read(buffer.array(), buffer.arrayOffset(), 8 ); buffer.rewind(); } dataSize = buffer.getInt(); checkFormat(dataSize > 0 , "wrong datasize: " + dataSize); return new WavInfo (new FormatSpec (rate, channels == 2 ), dataSize); }
上面的代码中,缺少的部分应该是显而易见的。正如你所看到的,仅仅支持16位,但在你可以修改代码以支持8位(AudioTrack 不支持任何其他分辨率的)。
下面这个方法,则是用来读取文件剩余的部分 - 音频数据 。1 2 3 4 5 6 public static byte [] readWavPcm(WavInfo info, InputStream stream) throws IOException { byte [] data = new byte [info.getDataSize()]; stream.read(data, 0 , data.length); return data; }
我们读取的WavInfo 结构体,包含采样率,分辨率和声道数已经足够让我们去播放我们读取的音频了。
如果我们不需要将全部音频数据一次性放入内存中,我们可以使用一个InputStream ,一点一点地读取。