package us.achromaticmetaphor.imcktg; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class WAVETone { private static final int FOURCC_RIFF = 0x46464952; private static final int FOURCC_WAVE = 0x45564157; private static final int FOURCC_FMT = 0x20746d66; private static final int FOURCC_DATA = 0x61746164; private final FileChannel channel; private final MappedByteBuffer head; private int dataOffset = -1; private int samplesPerSecond; private ByteBuffer silentSample; public WAVETone(File wav) throws IOException { channel = new RandomAccessFile(wav, "rw").getChannel(); MappedByteBuffer mapped = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); mapped.order(ByteOrder.LITTLE_ENDIAN); try { if (mapped.getInt(0) != FOURCC_RIFF || mapped.getInt(8) != FOURCC_WAVE) throw new IllegalArgumentException("not WAVE"); int fmtOffset = -1; for (int offset = 12; offset < mapped.capacity(); ) { int chunkId = mapped.getInt(offset); if (chunkId == FOURCC_FMT) fmtOffset = offset; if (chunkId == FOURCC_DATA) dataOffset = offset; offset += mapped.getInt(offset + 4) + 8; if ((offset & 1) != 0) offset++; } if (fmtOffset < 0) throw new IllegalArgumentException("no fmt chunk"); if (dataOffset < 0) throw new IllegalArgumentException("no data chunk"); if (mapped.getShort(fmtOffset + 8) != 1) throw new IllegalArgumentException("not PCM"); short nchannels = mapped.getShort(fmtOffset + 10); samplesPerSecond = mapped.getInt(fmtOffset + 12); short bitsPerSample = mapped.getShort(fmtOffset + 22); int bytesPerSample = nchannels * (bitsPerSample / 8) + ((bitsPerSample & 7) != 0 ? 1 : 0); silentSample = ByteBuffer.allocate(bytesPerSample); byte silentByte = (byte) (bitsPerSample < 9 ? (1 << (bitsPerSample - 1)) - 1 : 0); for (int i = 0; i < bytesPerSample; i++) silentSample.put(silentByte); } catch (IndexOutOfBoundsException ioobe) { throw new IllegalArgumentException("invalid WAVE", ioobe); } head = channel.map(FileChannel.MapMode.READ_WRITE, 0, dataOffset + 8); head.order(ByteOrder.LITTLE_ENDIAN); } private int dataSize() { return head.getInt(dataOffset + 4); } private MappedByteBuffer extendData(int bytes) throws IOException { int dataSize = dataSize(); long dataEnd = dataSize + dataOffset + 8; if ((dataEnd & 1) != 0) dataEnd++; MappedByteBuffer tail = channel.map(FileChannel.MapMode.READ_ONLY, dataEnd, channel.size() - dataEnd); MappedByteBuffer newTail = channel.map(FileChannel.MapMode.READ_WRITE, dataEnd + bytes, channel.size() - dataEnd); newTail.put(tail); newTail.force(); head.putInt(dataOffset + 4, dataSize + bytes); head.putInt(4, head.getInt(4) + bytes); head.force(); return channel.map(FileChannel.MapMode.READ_WRITE, dataSize + dataOffset + 8, bytes); } public void appendSilence(int milliseconds) throws IOException { int outsamples = milliseconds * samplesPerSecond / 1000; MappedByteBuffer extended = extendData(outsamples * silentSample.capacity()); for (int i = 0; i < outsamples; i++) { silentSample.rewind(); extended.put(silentSample); } extended.force(); } public void repeat(int count) throws IOException { int dataSize = dataSize(); MappedByteBuffer samples = channel.map(FileChannel.MapMode.READ_ONLY, dataOffset + 8, dataSize); MappedByteBuffer extended = extendData(dataSize * count); for (int i = 0; i < count; i++) { samples.rewind(); extended.put(samples); } extended.force(); } public void close() throws IOException { channel.close(); } }