// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.sound;
import java.util.Arrays;
import java.util.HashSet;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.util.DynamicArray;
/**
* Loads and provides access to uncompressed PCM WAV audio data.
*/
public class WavBuffer extends AudioBuffer
{
public WavBuffer(ResourceEntry entry) throws Exception
{
super(entry);
}
public WavBuffer(ResourceEntry entry, AudioOverride override) throws Exception
{
super(entry, override);
}
public WavBuffer(byte[] buffer, int offset) throws Exception
{
super(buffer, offset);
}
public WavBuffer(byte[] buffer, int offset, AudioOverride override) throws Exception
{
super(buffer, offset, override);
}
//--------------------- Begin Class AudioBuffer ---------------------
@Override
protected void convert(byte[] buffer, int offset, AudioOverride override) throws Exception
{
WaveFmt fmt = new WaveFmt(override);
offset = fmt.read(buffer, offset);
if (fmt.pcmType == WaveFmt.ID_TYPE_ADPCM)
data = convertADPCM(buffer, offset, fmt);
else if (fmt.pcmType == WaveFmt.ID_TYPE_PCM) {
byte[] header = createWAVHeader(fmt.samplesPerChannel, fmt.numChannels,
fmt.sampleRate, fmt.bitsPerSample);
int dstDataSize = fmt.samplesPerChannel * fmt.numChannels * (fmt.bitsPerSample / 8);
data = new byte[header.length + dstDataSize];
System.arraycopy(header, 0, data, 0, header.length);
System.arraycopy(buffer, offset, data, header.length, dstDataSize);
}
}
//--------------------- End Class AudioBuffer ---------------------
// Decodes IMA ADPCM encoded audio data and returns a buffer containing
// signed 16-bit PCM audio data, including WAV header
private static byte[] convertADPCM(byte[] buffer, int offset, WaveFmt fmt) throws Exception
{
final int stepTable[] = {
7, 8, 9, 10, 11, 12, 13, 14,
16, 17, 19, 21, 23, 25, 28, 31,
34, 37, 41, 45, 50, 55, 60, 66,
73, 80, 88, 97, 107, 118, 130, 143,
157, 173, 190, 209, 230, 253, 279, 307,
337, 371, 408, 449, 494, 544, 598, 658,
724, 796, 876, 963, 1060, 1166, 1282, 1411,
1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024,
3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
32767};
final int indexTable[] = {-1, -1, -1, -1, 2, 4, 6, 8};
// sanity checks
if (buffer == null || fmt == null)
throw new NullPointerException();
if (fmt.pcmType != WaveFmt.ID_TYPE_ADPCM)
throw new Exception("No ADPCM data found");
if (offset + fmt.dataSize > buffer.length)
throw new Exception("Input buffer too small");
// creating output buffer and header
int pcmBitsPerSample = 16;
int pcmDataSize = fmt.samplesPerChannel * fmt.numChannels * pcmBitsPerSample >> 3;
byte[] header = createWAVHeader(fmt.samplesPerChannel, fmt.numChannels, fmt.sampleRate, pcmBitsPerSample);
DynamicArray pcm = DynamicArray.allocate((header.length + pcmDataSize) >> 1, DynamicArray.ElementType.SHORT);
System.arraycopy(header, 0, pcm.getArray(), 0, header.length);
int pcmOfs = header.length >> 1;
// decoding data
int adpcmSize = fmt.dataSize;
int pcmBlockSize = (fmt.samplesPerBlock * fmt.numChannels);;
while (adpcmSize > 0) {
for (int channel = 0; channel < fmt.numChannels; channel++) {
short lastSample = DynamicArray.getShort(buffer, offset + (channel << 2));
byte stepIndex = DynamicArray.getByte(buffer, offset + (channel << 2) + 2);
pcm.putShort(pcmOfs + channel, lastSample);
for (int idx = 0; idx < (fmt.samplesPerBlock >> 1); idx++) {
int idxData = offset + idx + ((fmt.numChannels + channel + (fmt.numChannels - 1) * (idx >> 2)) << 2);
int pcmIdx = channel + fmt.numChannels * (1 + (idx << 1));
// Lower 4 bits
byte nibble = (byte)(buffer[idxData] & 0x07);
int diff = (stepTable[stepIndex] * nibble >> 2) + (stepTable[stepIndex] >> 3);
if ((buffer[idxData] & 0x08) != 0)
lastSample -= (short)diff;
else
lastSample += (short)diff;
stepIndex += (byte)indexTable[nibble];
if (stepIndex > stepTable.length - 1)
stepIndex = (byte)(stepTable.length - 1);
else if (stepIndex < 0)
stepIndex = (byte)0;
pcm.putShort(pcmOfs + pcmIdx, lastSample);
// Upper 4 bits
nibble = (byte)((buffer[idxData] >> 4) & 0x07);
diff = (stepTable[stepIndex] * nibble >> 2) + (stepTable[stepIndex] >> 3);
if ((buffer[idxData] & 0x80) != 0)
lastSample -= (short)diff;
else
lastSample += (short)diff;
stepIndex += (byte)indexTable[nibble];
if (stepIndex > stepTable.length - 1)
stepIndex = (byte)(stepTable.length - 1);
else if (stepIndex < 0)
stepIndex = (byte)0;
pcm.putShort(pcmOfs + fmt.numChannels + pcmIdx, lastSample);
}
}
adpcmSize -= fmt.blockAlign;
offset += fmt.blockAlign;
pcmOfs += pcmBlockSize;
}
return pcm.getArray();
}
//-------------------------- INNER CLASSES --------------------------
private static class WaveFmt
{
private static final int ID_CHUNK = 0x46464952; // 'RIFF'
private static final int ID_FORMAT = 0x45564157; // 'WAVE'
private static final int ID_SUBCHUNK1 = 0x20746d66; // 'fmt '
private static final int ID_SUBCHUNK2 = 0x61746164; // 'data'
private static final short ID_TYPE_PCM = 0x01; // PCM
private static final short ID_TYPE_ADPCM = 0x11; // IMA ADPCM
private static final HashSet<Short> s_audioTypes =
new HashSet<Short>(Arrays.asList(new Short[]{ID_TYPE_PCM, ID_TYPE_ADPCM}));
private final AudioOverride override;
private int sampleRate, samplesPerBlock, samplesPerChannel, dataSize;
private short pcmType, numChannels, blockAlign, bitsPerSample;
private WaveFmt(AudioOverride override)
{
if (override == null)
override = AudioOverride.override(0, 0, 0);
this.override = override;
}
// Read wave header and return start offset of audio data
private int read(byte[] buffer, int offset) throws Exception
{
if (buffer == null)
throw new NullPointerException();
if (offset < 0 || offset + 44 >= buffer.length)
throw new Exception("Input buffer too small");
if (DynamicArray.getInt(buffer, offset) != ID_CHUNK)
throw new Exception("Invalid RIFF header");
int i = DynamicArray.getInt(buffer, offset + 4);
if (i <= 36 || offset + i + 8 > buffer.length)
throw new Exception("Input buffer too small");
if (DynamicArray.getInt(buffer, offset + 8) != ID_FORMAT)
throw new Exception("Unsupported RIFF format");
offset += 12;
if (DynamicArray.getInt(buffer, offset) != ID_SUBCHUNK1)
throw new Exception("Invalid fmt header block");
int subChunk1Size = DynamicArray.getInt(buffer, offset + 4);
if (subChunk1Size < 16)
throw new Exception("Invalid fmt header size");
pcmType = DynamicArray.getShort(buffer, offset + 8);
if (!s_audioTypes.contains(pcmType))
throw new Exception("Unsupported audio compression format: " + pcmType);
numChannels = DynamicArray.getShort(buffer, offset + 10);
sampleRate = DynamicArray.getInt(buffer, offset + 12);
DynamicArray.getInt(buffer, offset + 16); // byte rate
blockAlign = DynamicArray.getShort(buffer, offset + 20);
bitsPerSample = DynamicArray.getShort(buffer, offset + 22);
offset += subChunk1Size + 8;
if (pcmType == ID_TYPE_ADPCM) { // ADPCM compression
if (override.numChannels > 0)
numChannels = (short)override.numChannels;
if (override.sampleRate > 0)
sampleRate = override.sampleRate;
if (bitsPerSample != 4)
throw new Exception("ADPCM: " + bitsPerSample + " bits/sample not supported");
short extraSize = DynamicArray.getShort(buffer, offset);
if (extraSize < 2)
throw new Exception("ADPCM: Extra header size too small");
if (extraSize < 4)
samplesPerBlock = DynamicArray.getShort(buffer, 38);
else
samplesPerBlock = DynamicArray.getInt(buffer, 38);
offset += extraSize + 2;
} else { // PCM compression
if (override.numChannels > 0)
numChannels = (short)override.numChannels;
if (override.sampleRate > 0)
sampleRate = override.sampleRate;
if (override.bitsPerSample > 0)
bitsPerSample = (short)override.bitsPerSample;
blockAlign = (short)(numChannels * bitsPerSample / 8);
samplesPerBlock = 1;
}
// skip additional info headers
while (DynamicArray.getInt(buffer, offset) != ID_SUBCHUNK2) {
int skip = DynamicArray.getInt(buffer, offset + 4);
offset += 8 + skip;
if (offset >= buffer.length)
throw new Exception("Unexpected end of data");
}
if (DynamicArray.getInt(buffer, offset) != ID_SUBCHUNK2)
throw new Exception("Invalid data header block");
dataSize = DynamicArray.getInt(buffer, offset + 4);
samplesPerChannel = (dataSize / blockAlign) * samplesPerBlock;
offset += 8;
return offset;
}
}
}