我们的目标是用一个InputStream,由其从一个WAV文件加载PCM数据,来提供原始字节数据。然后我们就可以将原始的PCM数据直接推送到使用已经正确的配置好了的AudioTrack.write,通过使用AudioTrack.write()这个API。
WAV文件包含一个文件头和具体数据会。我们需要读取文件头以知道诸如采样速率,分辨率等信息。另外,我们通过文件头,也可以知道此格式是否支持。WAV可以封装成多种格式,我们无法全部支持。也许,只是合理的采样率,分辨率和通道的线性PCM格式。
WAV格式的细节在互联网上都可以找到,你仅仅需要在Google上搜索下。但是,遗憾的是,我并没有搜索到一个很好的Java库来读取WAV文件,而且可以移植到Android下。因此,我自己写了一些简单的代码。
下面这个方法就是如何读取一个WAV文件的头部:
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); // 1 means // Linear // PCM 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) { // "data" marker 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不支持任何其他分辨率的)。
下面这个方法,则是用来读取文件剩余的部分 –音频数据。
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,一点一点地读取。
将PCM传入AudioTrack
我们现在面临2种情况,新建一个适合这种格式的AudioTrack,或者使用一个已存在的AudioTrack,但是可能和我们WAV音频数据的格式不一致。
在第一种情况,事情就很简单了,我们仅仅需要使用AudioTrack构造器构造一个我们已经从WAV头部对应的即可。
第二种情况,我们就需要将我们的音频变成AudioTrack需要的目标格式。我们需要做一下几种转换方式:
如果采样率不同,要么丢弃或复制一个样本以便和目标速率相匹配。如果分辨率是不同的,将源信号分辨率映射到目标分辨率,从16位到8位,反之亦然。如果信道不同,我们要么将立体声声道混合成一个单声道或重复单声道的数据把它变成准立体声。(请考虑将这些算法的实现放在Native层,因为Native层在做这类处理有很大的优势。)
在其他情况下,我们已经确定格式已经匹配。我们使用AudioTrack.write()写入缓冲区,以便实现回放。
记住,如果你使用静态模式,你需要在play()之前,新建一个包含准确的缓冲区大小的AudioTrack,同时写入write()音频数据。而在流模式下,我们可以先使用AudioTrack的play(),然后在使用write()写入数据部分
总结
你想实现AudioTrack上播放WAV音频可能有很多原因。有时候,可能是SoundPool有尺寸限制,或是MediaPlayer会有延迟和对资源占用太高,让你考虑使用这种方式。有时候你需要修改音频或者混合音频。