package org.jcodec.codecs.wav; import org.jcodec.api.UnhandledStateException; import org.jcodec.common.AudioFormat; import org.jcodec.common.JCodecUtil2; import org.jcodec.common.io.IOUtils; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.model.ChannelLabel; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * @author The JCodec project * */ public class WavHeader { public static class FmtChunkExtended extends FmtChunk { short cbSize; short bitsPerCodedSample; int channelLayout; int guid; public FmtChunkExtended(FmtChunk other, short cbSize, short bitsPerCodedSample, int channelLayout, int guid) { super(other.audioFormat, other.numChannels, other.sampleRate, other.byteRate, other.blockAlign, other.bitsPerSample); this.cbSize = cbSize; this.bitsPerCodedSample = bitsPerCodedSample; this.channelLayout = channelLayout; this.guid = guid; } public static FmtChunk read(ByteBuffer bb) throws IOException { FmtChunk fmtChunk = FmtChunk.get(bb); ByteOrder old = (ByteOrder) bb.order(); try { bb.order(ByteOrder.LITTLE_ENDIAN); return new FmtChunkExtended(fmtChunk, bb.getShort(), bb.getShort(), bb.getInt(), bb.getInt()); } finally { bb.order(old); } } public void put(ByteBuffer bb) throws IOException { super.put(bb); ByteOrder old = (ByteOrder) bb.order(); bb.order(ByteOrder.LITTLE_ENDIAN); bb.putShort(cbSize); bb.putShort(bitsPerCodedSample); bb.putInt(channelLayout); bb.putInt(guid); bb.order(old); } public int size() { return super.size() + 12; } public ChannelLabel[] getLabels() { List<ChannelLabel> labels = new ArrayList<ChannelLabel>(); for (int i = 0; i < mapping.length; i++) { if ((channelLayout & (1 << i)) != 0) labels.add(mapping[i]); } return labels.toArray(new ChannelLabel[0]); } } static ChannelLabel[] mapping = new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.CENTER, ChannelLabel.LFE, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT, ChannelLabel.FRONT_CENTER_LEFT, ChannelLabel.FRONT_CENTER_RIGHT, ChannelLabel.REAR_CENTER, ChannelLabel.SIDE_LEFT, ChannelLabel.SIDE_RIGHT, ChannelLabel.CENTER, ChannelLabel.FRONT_LEFT, ChannelLabel.CENTER, ChannelLabel.FRONT_RIGHT, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_CENTER, ChannelLabel.REAR_RIGHT, ChannelLabel.STEREO_LEFT, ChannelLabel.STEREO_RIGHT }; public static class FmtChunk { public short audioFormat; public short numChannels; public int sampleRate; public int byteRate; public short blockAlign; public short bitsPerSample; public FmtChunk(short audioFormat, short numChannels, int sampleRate, int byteRate, short blockAlign, short bitsPerSample) { this.audioFormat = audioFormat; this.numChannels = numChannels; this.sampleRate = sampleRate; this.byteRate = byteRate; this.blockAlign = blockAlign; this.bitsPerSample = bitsPerSample; } public static FmtChunk get(ByteBuffer bb) throws IOException { ByteOrder old = (ByteOrder) bb.order(); try { bb.order(ByteOrder.LITTLE_ENDIAN); return new FmtChunk(bb.getShort(), bb.getShort(), bb.getInt(), bb.getInt(), bb.getShort(), bb.getShort()); } finally { bb.order(old); } } public void put(ByteBuffer bb) throws IOException { ByteOrder old = (ByteOrder) bb.order(); bb.order(ByteOrder.LITTLE_ENDIAN); bb.putShort(audioFormat); bb.putShort(numChannels); bb.putInt(sampleRate); bb.putInt(byteRate); bb.putShort(blockAlign); bb.putShort(bitsPerSample); bb.order(old); } public int size() { return 16; } } public String chunkId; public int chunkSize; public String format; public FmtChunk fmt; public int dataOffset; public long dataSize; public static final int WAV_HEADER_SIZE = 44; public WavHeader(String chunkId, int chunkSize, String format, FmtChunk fmt, int dataOffset, long dataSize) { this.chunkId = chunkId; this.chunkSize = chunkSize; this.format = format; this.fmt = fmt; this.dataOffset = dataOffset; this.dataSize = dataSize; } public static WavHeader copyWithRate(WavHeader header, int rate) { WavHeader result = new WavHeader(header.chunkId, header.chunkSize, header.format, copyFmt(header.fmt), header.dataOffset, header.dataSize); result.fmt.sampleRate = rate; return result; } public static WavHeader copyWithChannels(WavHeader header, int channels) { WavHeader result = new WavHeader(header.chunkId, header.chunkSize, header.format, copyFmt(header.fmt), header.dataOffset, header.dataSize); result.fmt.numChannels = (short) channels; return result; } private static FmtChunk copyFmt(FmtChunk fmt) { if (fmt instanceof FmtChunkExtended) { FmtChunkExtended fmtext = (FmtChunkExtended) fmt; fmt = new FmtChunkExtended(fmtext, fmtext.cbSize, fmtext.bitsPerCodedSample, fmtext.channelLayout, fmtext.guid); } else { fmt = new FmtChunk(fmt.audioFormat, fmt.numChannels, fmt.sampleRate, fmt.byteRate, fmt.blockAlign, fmt.bitsPerSample); } return fmt; } /** * Creates wav header for the specified audio format * * @param format * @param samples */ public static WavHeader createWavHeader(AudioFormat format, int samples) { WavHeader w = new WavHeader("RIFF", 40, "WAVE", new FmtChunk((short) 1, (short) format.getChannels(), format.getSampleRate(), format.getSampleRate() * format.getChannels() * (format.getSampleSizeInBits() >> 3), (short) (format.getChannels() * (format.getSampleSizeInBits() >> 3)), (short) format.getSampleSizeInBits()), 44, calcDataSize(format.getChannels(), format.getSampleSizeInBits() >> 3, samples)); return w; } public static WavHeader stereo48k() { return stereo48kWithSamples(0); } public static WavHeader stereo48kWithSamples(long samples) { return new WavHeader("RIFF", 40, "WAVE", new FmtChunk((short) 1, (short) 2, 48000, 48000 * 2 * 16 / 8, (short) 4, (short) 16), 44, calcDataSize(2, 2, samples)); } public static WavHeader mono48k(long samples) { return new WavHeader("RIFF", 40, "WAVE", new FmtChunk((short) 1, (short) 1, 48000, 48000 * 1 * 16 / 8, (short) 2, (short) 16), 44, calcDataSize(1, 2, samples)); } public static WavHeader emptyWavHeader() { return new WavHeader("RIFF", 40, "WAVE", newFmtChunk(), 44, 0); } private static FmtChunk newFmtChunk() { return new FmtChunk((short) 1, (short) 0, 0, 0, (short) 0, (short) 0); } public static WavHeader read(File file) throws IOException { ReadableByteChannel is = null; try { is = NIOUtils.readableChannel(file); return readChannel(is); } finally { IOUtils.closeQuietly(is); } } public static WavHeader readChannel(ReadableByteChannel _in) throws IOException { ByteBuffer buf = ByteBuffer.allocate(128); buf.order(ByteOrder.LITTLE_ENDIAN); _in.read(buf); if (buf.remaining() > 0) throw new IOException("Incomplete wav header found"); buf.flip(); String chunkId = NIOUtils.readString(buf, 4); int chunkSize = buf.getInt(); String format = NIOUtils.readString(buf, 4); FmtChunk fmt = null; if (!"RIFF".equals(chunkId) || !"WAVE".equals(format)) { return null; } String fourcc; int size = 0; do { fourcc = NIOUtils.readString(buf, 4); size = buf.getInt(); if ("fmt ".equals(fourcc) && size >= 14 && size <= 1024 * 1024) { switch (size) { case 16: fmt = FmtChunk.get(buf); break; case 18: fmt = FmtChunk.get(buf); NIOUtils.skip(buf, 2); break; case 40: fmt = FmtChunkExtended.get(buf); NIOUtils.skip(buf, 12); break; case 28: fmt = FmtChunkExtended.get(buf); break; default: throw new UnhandledStateException("Don't know how to handle fmt size: " + size); } } else if (!"data".equals(fourcc)) { NIOUtils.skip(buf, size); } } while (!"data".equals(fourcc)); return new WavHeader(chunkId, chunkSize, format, fmt, buf.position(), size); } public static WavHeader multiChannelWavFromFiles(File... arguments) throws IOException { WavHeader headers[] = new WavHeader[arguments.length]; for (int i = 0; i < arguments.length; i++) { headers[i] = read(arguments[i]); } return multiChannelWav(headers); } /** Takes single channel wavs as input produces multi channel wav */ public static WavHeader multiChannelWav(WavHeader... arguments) { WavHeader w = emptyWavHeader(); int totalSize = 0; for (int i = 0; i < arguments.length; i++) { WavHeader wavHeader = arguments[i]; totalSize += wavHeader.dataSize; } w.dataSize = totalSize; FmtChunk fmt = arguments[0].fmt; int bitsPerSample = fmt.bitsPerSample; int bytesPerSample = bitsPerSample / 8; int sampleRate = (int) fmt.sampleRate; w.fmt.bitsPerSample = (short) bitsPerSample; w.fmt.blockAlign = (short) (arguments.length * bytesPerSample); w.fmt.byteRate = arguments.length * bytesPerSample * sampleRate; w.fmt.numChannels = (short) arguments.length; w.fmt.sampleRate = sampleRate; return w; } public void write(WritableByteChannel out) throws IOException { ByteBuffer bb = ByteBuffer.allocate(44); bb.order(ByteOrder.LITTLE_ENDIAN); long chunkSize; if (dataSize <= 0xffffffffL) { chunkSize = dataSize + 36; } else { chunkSize = 40; } bb.put(JCodecUtil2.asciiString("RIFF")); bb.putInt((int) chunkSize); bb.put(JCodecUtil2.asciiString("WAVE")); bb.put(JCodecUtil2.asciiString("fmt ")); bb.putInt(fmt.size()); fmt.put(bb); bb.put(JCodecUtil2.asciiString("data")); if (dataSize <= 0xffffffffL) { bb.putInt((int) dataSize); } else { bb.putInt(0); } bb.flip(); out.write(bb); } public static long calcDataSize(int numChannels, int bytesPerSample, long samples) { return samples * numChannels * bytesPerSample; } public static WavHeader create(AudioFormat af, int size) { WavHeader w = emptyWavHeader(); w.dataSize = size; FmtChunk fmt = newFmtChunk(); int bitsPerSample = af.getSampleSizeInBits(); int bytesPerSample = bitsPerSample / 8; int sampleRate = (int) af.getSampleRate(); w.fmt.bitsPerSample = (short) bitsPerSample; w.fmt.blockAlign = (short) (af.getFrameSize()); w.fmt.byteRate = (int) af.getFrameRate() * af.getFrameSize(); w.fmt.numChannels = (short) af.getChannels(); w.fmt.sampleRate = (int) af.getSampleRate(); return w; } public ChannelLabel[] getChannelLabels() { if (fmt instanceof FmtChunkExtended) { return ((FmtChunkExtended) fmt).getLabels(); } else { switch (fmt.numChannels) { case 1: return new ChannelLabel[] { ChannelLabel.MONO }; case 2: return new ChannelLabel[] { ChannelLabel.STEREO_LEFT, ChannelLabel.STEREO_RIGHT }; case 3: return new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.REAR_CENTER }; case 4: return new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT }; case 5: return new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.CENTER, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT }; case 6: return new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.CENTER, ChannelLabel.LFE, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT }; case 7: return new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.CENTER, ChannelLabel.LFE, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT, ChannelLabel.REAR_CENTER }; case 8: return new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.CENTER, ChannelLabel.LFE, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT }; default: ChannelLabel[] labels = new ChannelLabel[fmt.numChannels]; Arrays.fill(labels, ChannelLabel.MONO); return labels; } } } public AudioFormat getFormat() { return new AudioFormat(fmt.sampleRate, fmt.bitsPerSample, fmt.numChannels, true, false); } }