Java tutorial
/******************************************************************************* * sdrtrunk * Copyright (C) 2014-2017 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * ******************************************************************************/ package record.wave; import org.apache.commons.lang3.Validate; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.sound.sampled.AudioFormat; public class WaveWriter implements AutoCloseable { private static final Pattern FILENAME_PATTERN = Pattern.compile("(.*_)(\\d+)(\\.wav)"); public static final long MAX_WAVE_SIZE = 2l * (long) Integer.MAX_VALUE; private AudioFormat mAudioFormat; private int mFileRolloverCounter = 1; private long mMaxSize; private Path mFile; private FileChannel mFileChannel; /** * Constructs a new wave writer that is open with a complete header, ready * for writing buffers of PCM sample data. * * Each time the maximum file size is reached, a new file is created with a * series suffix appended to the file name. * * @param format - audio format (channels, sample size, sample rate) * @param file - wave file to write * @param maxSize - maximum file size ( range: 1 - 4,294,967,294 bytes ) * @throws IOException - if there are any IO issues */ public WaveWriter(AudioFormat format, Path file, long maxSize) throws IOException { Validate.isTrue(format != null); Validate.isTrue(file != null); mAudioFormat = format; mFile = file; if (0 < maxSize && maxSize <= MAX_WAVE_SIZE) { mMaxSize = maxSize; } else { mMaxSize = MAX_WAVE_SIZE; } open(); } /** * Constructs a new wave writer that is open with a complete header, ready * for writing buffers of PCM sample data. The maximum file size is limited * to the max size specified in the wave file format: max unsigned integer * * @param format - audio format (channels, sample size, sample rate) * @param file - wave file to write * @throws IOException - if there are any IO issues */ public WaveWriter(AudioFormat format, Path file) throws IOException { this(format, file, Integer.MAX_VALUE * 2); } /** * Opens the file and writes a wave header. */ private void open() throws IOException { int version = 2; while (Files.exists(mFile)) { mFile = Paths.get(mFile.toFile().getAbsolutePath().replace(".wav", "_" + version + ".wav")); version++; } mFileChannel = (FileChannel.open(mFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)); ByteBuffer header = WaveUtils.getWaveHeader(mAudioFormat); header.flip(); while (header.hasRemaining()) { mFileChannel.write(header); } } /** * Closes the file */ public void close() throws IOException { mFileChannel.force(true); mFileChannel.close(); mFileChannel = null; } @Override protected void finalize() throws IOException { mFileChannel.force(true); mFileChannel.close(); mFileChannel = null; } /** * Writes the buffer contents to the file. Assumes that the buffer is full * and the first byte of data is at position 0. */ public void write(ByteBuffer buffer) throws IOException { buffer.position(0); /* Write the full buffer if there is room, respecting the max file size */ if (mFileChannel.size() + buffer.capacity() < mMaxSize) { while (buffer.hasRemaining()) { mFileChannel.write(buffer); } updateWaveFileSize(); } else { /* Split the buffer to finish filling the current file and then put * the leftover into a new file */ int remaining = (int) (mMaxSize - mFileChannel.size()); /* Ensure we write full frames to fill up the remaining size */ remaining -= (int) (remaining % mAudioFormat.getFrameSize()); byte[] bytes = buffer.array(); ByteBuffer current = ByteBuffer.wrap(Arrays.copyOf(bytes, remaining)); ByteBuffer next = ByteBuffer.wrap(Arrays.copyOfRange(bytes, remaining, bytes.length)); while (current.hasRemaining()) { mFileChannel.write(current); } updateWaveFileSize(); rollover(); while (next.hasRemaining()) { mFileChannel.write(next); } updateWaveFileSize(); } } /** * Closes out the current file, appends an incremented sequence number to * the file name and opens up a new file. */ private void rollover() throws IOException { close(); mFileRolloverCounter++; updateFileName(); open(); } /** * Updates the overall and the chunk2 sizes */ private void updateWaveFileSize() throws IOException { /* Update overall wave size (total size - 8 bytes) */ ByteBuffer buffer = getUnsignedIntegerBuffer(mFileChannel.size() - 8); mFileChannel.write(buffer, 4); ByteBuffer buffer2 = getUnsignedIntegerBuffer(mFileChannel.size() - 44); mFileChannel.write(buffer2, 40); } /** * Creates a little-endian 4-byte buffer containing an unsigned 32-bit * integer value derived from the 4 least significant bytes of the argument. * * The buffer's position is set to 0 to prepare it for writing to a channel. */ protected static ByteBuffer getUnsignedIntegerBuffer(long size) { ByteBuffer buffer = ByteBuffer.allocate(4); buffer.put((byte) (size & 0xFFl)); buffer.put((byte) (Long.rotateRight(size & 0xFF00l, 8))); buffer.put((byte) (Long.rotateRight(size & 0xFF0000l, 16))); /* This side-steps an issue with right shifting a signed long by 32 * where it produces an error value. Instead, we right shift in two steps. */ buffer.put((byte) Long.rotateRight(Long.rotateRight(size & 0xFF000000l, 16), 8)); buffer.position(0); return buffer; } public static String toString(ByteBuffer buffer) { StringBuilder sb = new StringBuilder(); byte[] bytes = buffer.array(); for (byte b : bytes) { sb.append(String.format("%02X ", b)); sb.append(" "); } return sb.toString(); } /** * Updates the current file name with the rollover counter series suffix */ private void updateFileName() { String filename = mFile.toString(); if (mFileRolloverCounter == 2) { filename = filename.replace(".wav", "_2.wav"); } else { Matcher m = FILENAME_PATTERN.matcher(filename); if (m.find()) { StringBuilder sb = new StringBuilder(); sb.append(m.group(1)); sb.append(mFileRolloverCounter); sb.append(m.group(3)); filename = sb.toString(); } } mFile = Paths.get(filename); } }