Created
July 2, 2014 20:38
-
-
Save fkaa/12cda897ba3a25b85884 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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