/*******************************************************************************
* 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 );
}
}