mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 23:57:25 -05:00
286 lines
7.3 KiB
Java
286 lines
7.3 KiB
Java
/*******************************************************************************
|
|
* 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 );
|
|
}
|
|
}
|