Skip to content

Instantly share code, notes, and snippets.

@fkaa
Created July 2, 2014 20:38
Show Gist options
  • Select an option

  • Save fkaa/12cda897ba3a25b85884 to your computer and use it in GitHub Desktop.

Select an option

Save fkaa/12cda897ba3a25b85884 to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2014 Felix Kaaman
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source
* distribution.
*/
package ludum.media;
import ludum.util.Convert;
import ludum.util.FourCC;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static org.lwjgl.openal.AL10.*;
/**
* Class for loading Waveform Audio files.
*
* @author Felix Kaaman
* @since 1.0
*/
@Resource(id="Waveform Audio File", extensions={".wav", ".wave"})
public class Wav implements Disposable {
private static final FourCC FMT_CHUNK_ID = FourCC.from("fmt ", ByteOrder.LITTLE_ENDIAN);
private static final FourCC DATA_CHUNK_ID = FourCC.from("data", ByteOrder.LITTLE_ENDIAN);
private static final FourCC RIFF_CHUNK_ID = FourCC.from("RIFF", ByteOrder.LITTLE_ENDIAN);
private static final FourCC RIFF_TYPE_ID = FourCC.from("WAVE", ByteOrder.LITTLE_ENDIAN);
private ByteBuffer data;
private WavInfo info;
public Wav(InputStream is) throws IOException, WavException {
this(is, 4096);
}
public Wav(InputStream is, int bufsize) throws IOException, WavException {
byte[] buffer = new byte[bufsize];
int bytes_read = is.read(buffer, 0, 12);
if (bytes_read != 12) throw new WavException("missing header.");
long riff_c_id = Convert.fromLittleEndian(buffer, 0, 4);
long chunk_size = Convert.fromLittleEndian(buffer, 4, 4);
long riff_t_id = Convert.fromLittleEndian(buffer, 8, 4);
if (RIFF_CHUNK_ID.equals(riff_c_id)) throw new WavException("RIFF chunk does not match");
if (RIFF_TYPE_ID.equals(riff_t_id)) throw new WavException("RIFF type does not match");
while ((bytes_read = is.read(buffer, 0, 8)) != -1) {
if (bytes_read != 8) throw new WavException("couldn't read chunk header");
long chunk_id = Convert.fromLittleEndian(buffer, 0, 4);
chunk_size = Convert.fromLittleEndian(buffer, 4, 4);
long num_chunk_bytes = chunk_size + (chunk_size & 0x1);
if (FMT_CHUNK_ID.equals(chunk_id)) {
bytes_read = is.read(buffer, 0, 16);
int compression = (int)Convert.fromLittleEndian(buffer, 0, 2);
if (compression != 1) throw new WavException("compression code " + compression + " is not supported");
int num_channels = (int)Convert.fromLittleEndian(buffer, 2, 2);
long sample_rate = Convert.fromLittleEndian(buffer, 4, 4);
//TODO: add to info
long avg_bytes = Convert.fromLittleEndian(buffer, 8, 4);
int block_align = (int)Convert.fromLittleEndian(buffer, 12, 2);
int bits_per_sample = (int)Convert.fromLittleEndian(buffer, 14, 2);
if (num_channels == 0) throw new WavException("number of channels must be greater than 0");
if (block_align == 0) throw new WavException("block align must be greater than 0");
if (bits_per_sample < 2 || bits_per_sample > 64) throw new WavException("valid bits must be >=2 and <=64");
int bytes_per_sample = (bits_per_sample + 7) / 8;
if (bytes_per_sample * num_channels != block_align) throw new WavException("incorrect block align for bits per sample");
info = new WavInfo(num_channels, sample_rate, block_align, bits_per_sample, bytes_per_sample);
num_chunk_bytes -= 16;
if (num_chunk_bytes > 0) is.skip(num_chunk_bytes);
} else if (DATA_CHUNK_ID.equals(chunk_id)) {
if (info == null) throw new WavException("Unexpected EOF, could not find format chunk");
if ((chunk_size & 0x1) != 0) throw new WavException("data chunk size is not a multiple of block align: " + info.block_align);
info.num_frames = chunk_size / info.block_align;
break;
} else {
is.skip(num_chunk_bytes);
}
}
if (info == null) throw new WavException("found EOF, expected fmt chunk");
if (info.num_frames == 0) throw new WavException("found EOF, expected data chunk");
data = ByteBuffer.allocateDirect((int) (info.num_frames * info.channels * info.bytes_per_sample));
int buf = 0;
bytes_read = 0;
long frame_count = 0;
for (int i = 0; i < info.num_frames; i++) {
for (int c = 0; c < info.channels; c++) {
//long sample = 0;
for (int b = 0; b < info.bytes_per_sample; b++) {
if (buf == bytes_read) {
int read = is.read(buffer, 0, bufsize);
if (read != -1) {
bytes_read = read;
buf = 0;
} else {
throw new WavException("not enough data");
}
}
int v = buffer[buf];
if (b < info.bytes_per_sample - 1 || info.bytes_per_sample == 1) v &= 0xFF;
//sample += v << (b * 8);
data.put((byte) v);
buf++;
}
frame_count++;
}
}
data.rewind();
is.close();
}
/**
* The format in OpenAL
*
* @return the audio format
*/
public int getFormat() {
return info.significant_bits_per_sample == 16 ? (info.channels == 2 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16) : (info.channels == 2 ? AL_FORMAT_STEREO8 : AL_FORMAT_MONO8);
}
/**
* The number of channels specifies how many separate audio signals that are
* encoded in the wave data chunk. A value of 1 means a mono signal, a value
* of 2 means a stereo signal, etc.
*
* @return the number of channels
*/
public int getChannels() {
return info.channels;
}
/**
* The number of sample slices per second. This value is unaffected by the
* number of channels.
*
* @return the samplerate
*/
public int getSampleRate() {
return (int) info.sample_rate;
}
/**
* @return audio data
*/
public ByteBuffer getData() {
return data;
}
@Override
public void dispose() {
data.clear();
}
@Override
public String toString() {
return info.toString();
}
/**
* Custom exception for loading wav files.
*
* @since 1.0
*/
public class WavException extends IOException {
public WavException(String s) {
super("Invalid wav file, " + s);
}
}
/**
* Contains information about how the waveform data is stored and should be
* played back including the type of compression used, number of channels,
* sample rate, bits per sample and other attributes.
*
* @since 1.0
*/
protected class WavInfo {
public int channels;
public long sample_rate;
public int block_align;
public int significant_bits_per_sample;
public int bytes_per_sample;
public long num_frames;
public WavInfo(int channels, long sample_rate, int block_align, int bits_per_sample, int bytes_per_sample) {
this.channels = channels;
this.sample_rate = sample_rate;
this.block_align = block_align;
this.significant_bits_per_sample = bits_per_sample;
this.bytes_per_sample = bytes_per_sample;
}
@Override
public String toString() {
return String.format("[channels=%s, sample rate=%s, block align=%s, valid bits=%s]", channels, sample_rate, block_align, significant_bits_per_sample);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment