Interim checkin for call recording.

This commit is contained in:
denny
2015-09-11 14:01:47 -04:00
parent 0f85b7260a
commit 575197bd98
93 changed files with 2349 additions and 2637 deletions

View File

@@ -30,6 +30,7 @@ public class RecordComponentEditor extends AbstractChannelEditor
private JCheckBox mAudioRecorder = new JCheckBox( "Audio" );
private JCheckBox mBasebandRecorder = new JCheckBox( "Baseband I/Q" );
private JCheckBox mTrafficBasebandRecorder = new JCheckBox( "Traffic Channel Baseband I/Q" );
public RecordComponentEditor( ChannelNode channelNode )
{
@@ -37,6 +38,7 @@ public class RecordComponentEditor extends AbstractChannelEditor
add( mAudioRecorder, "span" );
add( mBasebandRecorder, "span" );
add( mTrafficBasebandRecorder, "span" );
reset();
}
@@ -58,6 +60,12 @@ public class RecordComponentEditor extends AbstractChannelEditor
{
config.addRecorder( RecorderType.BASEBAND );
}
if( mTrafficBasebandRecorder.isSelected() )
{
config.addRecorder( RecorderType.BASEBAND );
}
}
@Override
@@ -76,6 +84,9 @@ public class RecordComponentEditor extends AbstractChannelEditor
mBasebandRecorder.setSelected( config.getRecorders()
.contains( RecorderType.BASEBAND ) );
mTrafficBasebandRecorder.setSelected( config.getRecorders()
.contains( RecorderType.TRAFFIC_BASEBAND ) );
}
});
}

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014,2015 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
@@ -17,56 +17,174 @@
******************************************************************************/
package record;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import record.config.RecordConfiguration;
import record.wave.ComplexWaveRecorder;
import record.wave.FloatWaveRecorder;
import util.TimeStamp;
import record.wave.ComplexBufferWaveRecorder;
import record.wave.RealBufferWaveRecorder;
import sample.Listener;
import audio.AudioPacket;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
public class RecorderManager
public class RecorderManager implements Listener<AudioPacket>
{
public static final int sSAMPLE_RATE = 48000;
private static final Logger mLog = LoggerFactory.getLogger( RecorderManager.class );
public RecorderManager()
public static final int AUDIO_SAMPLE_RATE = 48000;
private static final long RECORDER_INACTIVITY_THRESHOLD = 10000; //1 minute in millis
// private static final RealBuffer AUDIO_SEPARATOR = new AudioSeparator();
private Map<String,RealBufferWaveRecorder> mRecorders = new HashMap<>();
private ThreadPoolManager mThreadPoolManager;
public RecorderManager( ThreadPoolManager threadPoolManager )
{
mThreadPoolManager = threadPoolManager;
}
@Override
public void receive( AudioPacket audioPacket )
{
if( audioPacket.hasAudioMetadata() &&
audioPacket.getAudioMetadata().isRecordable() )
{
String identifier = audioPacket.getAudioMetadata().getIdentifier();
if( mRecorders.containsKey( identifier ) )
{
RealBufferWaveRecorder recorder = mRecorders.get( identifier );
if( audioPacket.getType() == AudioPacket.Type.AUDIO )
{
recorder.receive( audioPacket.getAudioBuffer() );
}
else if( audioPacket.getType() == AudioPacket.Type.END )
{
recorder.stop();
mRecorders.remove( identifier );
}
}
else
{
if( audioPacket.getType() == AudioPacket.Type.AUDIO )
{
String filePrefix = getFilePrefix( audioPacket );
RealBufferWaveRecorder recorder =
new RealBufferWaveRecorder( mThreadPoolManager,
AUDIO_SAMPLE_RATE, filePrefix );
recorder.start();
recorder.receive( audioPacket.getAudioBuffer() );
mRecorders.put( identifier, recorder );
}
}
}
}
/**
* Constructs a file name and path for an audio recording
*/
private String getFilePrefix( AudioPacket packet )
{
StringBuilder sb = new StringBuilder();
sb.append( SystemProperties.getInstance()
.getApplicationFolder( "recordings" ) );
sb.append( File.separator );
Metadata systemMetadata = packet.getAudioMetadata()
.getMetadata( MetadataType.SYSTEM );
sb.append( systemMetadata != null ?
systemMetadata.getValue() : "UNKNOWN_SYSTEM" );
Metadata toMetadata = packet.getAudioMetadata()
.getMetadata( MetadataType.TO );
if( toMetadata != null )
{
sb.append( "_TO_" );
sb.append( toMetadata.getValue() );
}
Metadata fromMetadata = packet.getAudioMetadata()
.getMetadata( MetadataType.FROM );
if( fromMetadata != null )
{
sb.append( "_FROM_" );
sb.append( fromMetadata.getValue() );
}
return sb.toString();
}
//TODO: make this return modules
public List<Recorder> getRecorders( RecordConfiguration config )
/**
* Returns a list of recorder modules for use in a processing chain.
* Note: currently this only returns a baseband recorder.
*
* Create an instance of this RecorderManager class to listen to and centrally
* record all decoded audio.
*/
public static ComplexBufferWaveRecorder getBasebandRecorder(
ThreadPoolManager threadPoolManager, String channelName )
{
/* Note: the file suffix (ie .wav) gets added by the recorder */
//Get the base recording filename
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder();
sb.append( SystemProperties.getInstance()
.getApplicationFolder( "recordings" ) );
sb.append( File.separator );
sb.append( TimeStamp.getTimeStamp( "_" ) );
sb.append( "_" );
// sb.append( channel.getChannel().getName() );
ArrayList<Recorder> retVal = new ArrayList<Recorder>();
sb.append( channelName );
sb.append( "_baseband" );
for( RecorderType recorder: config.getRecorders() )
return new ComplexBufferWaveRecorder( threadPoolManager,
AUDIO_SAMPLE_RATE, sb.toString() );
}
/**
* Removes any recorders where the recent activity timestamp has exceeded
* the retention threshold.
*/
public class RecorderProcessor implements Runnable
{
@Override
public void run()
{
switch( recorder )
if( !mRecorders.isEmpty() )
{
case AUDIO:
retVal.add( new FloatWaveRecorder( sSAMPLE_RATE,
sb.toString() + "_audio" ) );
break;
case BASEBAND:
retVal.add( new ComplexWaveRecorder( sSAMPLE_RATE,
sb.toString() + "_baseband" ) );
break;
List<String> keysToRemove = new ArrayList<>();
for( Entry<String,RealBufferWaveRecorder> entry: mRecorders.entrySet() )
{
if( entry.getValue().getLastBufferReceived() +
RECORDER_INACTIVITY_THRESHOLD < System.currentTimeMillis() )
{
keysToRemove.add( entry.getKey() );
}
}
for( String key: keysToRemove )
{
RealBufferWaveRecorder recorder = mRecorders.remove( key );
recorder.dispose();
}
}
}
return retVal;
}
}

View File

@@ -20,7 +20,8 @@ package record;
public enum RecorderType
{
AUDIO( "Audio (.wav)"),
BASEBAND( "Baseband I/Q (.wav)");
BASEBAND( "Baseband I/Q (.wav)"),
TRAFFIC_BASEBAND( "Traffic Channel Baseband I/Q (.wav)" );
private String mDisplayString;

View File

@@ -1,181 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;
import javax.sound.sampled.AudioFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.Recorder;
import record.RecorderType;
import sample.Listener;
import util.waveaudio.WaveWriter;
import buffer.FloatSampleBufferAssembler;
/**
* Threaded WAVE audio recorder for recording mono short samples to a wave file
*/
public class BooleanWaveRecorder extends Recorder implements Listener<Boolean>
{
private final static Logger mLog =
LoggerFactory.getLogger( BooleanWaveRecorder.class );
private AudioFormat mAudioFormat;
private boolean mRunning = false;
private boolean mPaused = false;
private WaveWriter mWriter;
private String mFileName;
private BufferProcessor mBufferProcessor;
//Sample buffer queue ... constrained to 512 sample buffers (arbitrary size)
private LinkedBlockingQueue<ByteBuffer> mToWriteBufferQueue =
new LinkedBlockingQueue<ByteBuffer>( 512 );
private FloatSampleBufferAssembler mCurrentBuffer;
public BooleanWaveRecorder( int sampleRate, String filename )
{
super( RecorderType.AUDIO );
mAudioFormat = new AudioFormat( sampleRate, //SampleRate
16, //Sample Size
1, //Channels
true, //Signed
false ); //Little Endian
mCurrentBuffer = new FloatSampleBufferAssembler( sampleRate );
mFileName = filename;
}
@Override
public String getFileName()
{
return mFileName + ".wav";
}
public void start() throws IOException
{
if( !mRunning )
{
mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat );
mWriter.start();
mBufferProcessor = new BufferProcessor();
mBufferProcessor.start();
mRunning = true;
}
}
public void stop() throws IOException
{
if( mRunning )
{
mBufferProcessor = null;
mWriter.stop();
mWriter = null;
mRunning = false;
}
}
public void pause()
{
mPaused = true;
}
public void resume()
{
mPaused = false;
}
/**
* Assumes that the byte[] represents samples corresponding to the
* audio format object defined at construction
*/
public void receive( ByteBuffer buffer )
{
if( mRunning && !mPaused )
{
boolean success = mToWriteBufferQueue.offer( buffer );
if( !success )
{
mLog.error( "Wave recorder buffer overflow - throwing away " +
buffer.capacity() + " samples" );
}
}
}
@Override
public void receive( Boolean sample )
{
/**
* If the sample is true, put short value of 5000, otherwise
* put a short value of -5000
*/
mCurrentBuffer.put( sample ? 5000 : (short)-5000 );
if( !mCurrentBuffer.hasRemaining() )
{
//Dispatch the buffer to be written to disk
if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) )
{
//We had an overflow ... log it
mLog.error( "ShortWaveRecorder: buffer overflow - throwing "
+ "away 1 second of sample data" );
}
mCurrentBuffer.reset();
}
}
public class BufferProcessor extends Thread
{
public void run()
{
while( true )
{
try
{
if( mWriter != null )
{
mWriter.write( mToWriteBufferQueue.take() );
}
}
catch ( IOException ioe )
{
mLog.error( "IOException while trying to write to the "
+ "wave writer", ioe );
}
catch ( InterruptedException e )
{
mLog.error( "Oops ... error while processing the buffer "
+ "queue for the wave recorder", e );
}
}
}
}
}

View File

@@ -0,0 +1,225 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.sound.sampled.AudioFormat;
import module.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.complex.ComplexBuffer;
import sample.complex.IComplexBufferListener;
import util.TimeStamp;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/**
* WAVE audio recorder module for recording complex (I&Q) samples to a wave file
*/
public class ComplexBufferWaveRecorder extends Module
implements IComplexBufferListener, Listener<ComplexBuffer>
{
private final static Logger mLog =
LoggerFactory.getLogger( ComplexBufferWaveRecorder.class );
private WaveWriter mWriter;
private String mFilePrefix;
private Path mFile;
private AudioFormat mAudioFormat;
private ThreadPoolManager mThreadPoolManager;
private BufferProcessor mBufferProcessor;
private ScheduledFuture<?> mProcessorHandle;
private LinkedBlockingQueue<ComplexBuffer> mBuffers = new LinkedBlockingQueue<>( 500 );
public ComplexBufferWaveRecorder( ThreadPoolManager threadPoolManager,
int sampleRate, String filePrefix )
{
mThreadPoolManager = threadPoolManager;
mAudioFormat = new AudioFormat( sampleRate, //SampleRate
16, //Sample Size
2, //Channels
true, //Signed
false ); //Little Endian
mFilePrefix = filePrefix;
}
/**
* Converts the float samples in a complex buffer to a little endian 16-bit
* buffer
*/
public static ByteBuffer convert( ComplexBuffer buffer )
{
float[] samples = buffer.getSamples();
ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 );
converted.order( ByteOrder.LITTLE_ENDIAN );
for( float sample: samples )
{
converted.putShort( (short)( sample * Short.MAX_VALUE ) );
}
return converted;
}
public Path getFile()
{
return mFile;
}
public void start()
{
if( !running() )
{
if( mBufferProcessor == null )
{
mBufferProcessor = new BufferProcessor();
}
try
{
StringBuilder sb = new StringBuilder();
sb.append( mFilePrefix );
sb.append( "_" );
sb.append( TimeStamp.getTimeStamp( "_" ) );
sb.append( ".wav" );
mFile = Paths.get( sb.toString() );
mWriter = new WaveWriter( mAudioFormat, mFile );
/* Schedule the processor to run every 500 milliseconds */
mProcessorHandle = mThreadPoolManager.scheduleFixedRate(
ThreadType.BASEBAND_RECORDING, mBufferProcessor, 500,
TimeUnit.MILLISECONDS );
}
catch( IOException io )
{
mLog.error( "Error starting complex baseband recorder", io );
}
}
}
public boolean running()
{
return mProcessorHandle != null;
}
public void stop()
{
if( running() )
{
mThreadPoolManager.cancel( mProcessorHandle );
mProcessorHandle = null;
if( mWriter != null )
{
try
{
mWriter.close();
}
catch( IOException io )
{
mLog.error( "Error stopping complex wave recorder [" +
getFile() + "]", io );
}
}
}
mBuffers.clear();
mWriter = null;
mFile = null;
}
@Override
public void receive( ComplexBuffer buffer )
{
if( running() )
{
boolean success = mBuffers.offer( buffer );
if( !success )
{
mLog.error( "ComplexWaveRecorder: buffer overflow or error - throwing "
+ "away a buffer of sample data" );
stop();
}
}
}
@Override
public Listener<ComplexBuffer> getComplexBufferListener()
{
return this;
}
@Override
public void dispose()
{
stop();
mThreadPoolManager = null;
}
@Override
public void reset()
{
}
public class BufferProcessor implements Runnable
{
public void run()
{
try
{
ComplexBuffer buffer = mBuffers.poll();
while( buffer != null )
{
mWriter.write( convert( buffer ) );
buffer = mBuffers.poll();
}
}
catch ( IOException ioe )
{
/* Stop this module if/when we get an IO exception */
stop();
mLog.error( "IOException while trying to write to the wave "
+ "writer", ioe );
}
}
}
}

View File

@@ -1,199 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.LinkedBlockingQueue;
import javax.sound.sampled.AudioFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.Recorder;
import record.RecorderType;
import sample.Listener;
import sample.complex.ComplexBuffer;
import util.waveaudio.WaveWriter;
/**
* Threaded WAVE audio recorder for recording complex (I&Q) samples to a wave
* file
*/
public class ComplexWaveRecorder extends Recorder implements Listener<ComplexBuffer>
{
private final static Logger mLog =
LoggerFactory.getLogger( ComplexWaveRecorder.class );
private AudioFormat mAudioFormat;
private boolean mRunning = false;
private boolean mPaused = false;
private WaveWriter mWriter;
private String mFileName;
private BufferProcessor mBufferProcessor;
private Listener<ComplexBuffer> mListener;
//Sample buffer queue ... constrained to 512 sample buffers (arbitrary size)
private LinkedBlockingQueue<ByteBuffer> mToWriteBufferQueue =
new LinkedBlockingQueue<ByteBuffer>( 512 );
public ComplexWaveRecorder( int sampleRate, String filename )
{
super( RecorderType.AUDIO );
mAudioFormat = new AudioFormat( sampleRate, //SampleRate
16, //Sample Size
2, //Channels
true, //Signed
false ); //Little Endian
mFileName = filename;
}
/**
* Converts the float samples in a complex buffer to a little endian 16-bit
* buffer
*/
public static ByteBuffer convert( ComplexBuffer buffer )
{
float[] samples = buffer.getSamples();
ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 );
converted.order( ByteOrder.LITTLE_ENDIAN );
for( float sample: samples )
{
converted.putShort( (short)( sample * Short.MAX_VALUE ) );
}
return converted;
}
@Override
public String getFileName()
{
return mFileName + ".wav";
}
public void start() throws IOException
{
if( !mRunning )
{
mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat );
mWriter.start();
mBufferProcessor = new BufferProcessor();
mBufferProcessor.start();
mRunning = true;
}
}
public void stop() throws IOException
{
if( mRunning )
{
mBufferProcessor = null;
mWriter.stop();
mWriter = null;
mRunning = false;
}
}
public void pause()
{
mPaused = true;
}
public void resume()
{
mPaused = false;
}
/**
* Assumes that the byte[] represents samples corresponding to the
* audio format object defined at construction
*/
public void receive( ByteBuffer buffer )
{
if( mRunning && !mPaused )
{
boolean success = mToWriteBufferQueue.offer( buffer );
if( !success )
{
mLog.error( "Wave recorder buffer overflow - throwing away " +
buffer.capacity() + " samples" );
}
}
}
@Override
public void receive( ComplexBuffer buffer )
{
//Dispatch the buffer to be written to disk
if( !mToWriteBufferQueue.offer( convert( buffer ) ) )
{
//We had an overflow ... log it
mLog.error( "ComplexWaveRecorder: buffer overflow - throwing "
+ "away a buffer of sample data" );
}
if( mListener != null )
{
mListener.receive( buffer );
}
}
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
public class BufferProcessor extends Thread
{
public void run()
{
while( true )
{
try
{
if( mWriter != null )
{
mWriter.write( mToWriteBufferQueue.take() );
}
}
catch ( IOException ioe )
{
mLog.error( "IOException while trying to write to the wave "
+ "writer", ioe );
}
catch ( InterruptedException e )
{
mLog.error( "Oops ... error while processing the buffer "
+ "queue for the wave recorder", e );
}
}
}
}
}

View File

@@ -1,222 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.LinkedBlockingQueue;
import javax.sound.sampled.AudioFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.Recorder;
import record.RecorderType;
import sample.Listener;
import sample.real.RealBuffer;
import sample.real.RealSampleListener;
import util.waveaudio.WaveWriter;
import buffer.FloatSampleBufferAssembler;
/**
* Threaded WAVE audio recorder for recording mono float samples to a wave file
*/
public class FloatWaveRecorder extends Recorder implements RealSampleListener,
Listener<RealBuffer>
{
private final static Logger mLog =
LoggerFactory.getLogger( FloatWaveRecorder.class );
private AudioFormat mAudioFormat;
private boolean mRunning = false;
private boolean mPaused = false;
private WaveWriter mWriter;
private String mFileName;
private BufferProcessor mBufferProcessor;
//Sample buffer queue ... constrained to 512 sample buffers (arbitrary size)
private LinkedBlockingQueue<ByteBuffer> mToWriteBufferQueue =
new LinkedBlockingQueue<ByteBuffer>( 512 );
private FloatSampleBufferAssembler mCurrentBuffer;
private Listener<RealBuffer> mListener;
public FloatWaveRecorder( int sampleRate, String filename )
{
super( RecorderType.AUDIO );
mAudioFormat = new AudioFormat( sampleRate, //SampleRate
16, //Sample Size
1, //Channels
true, //Signed
false ); //Little Endian
mCurrentBuffer = new FloatSampleBufferAssembler( sampleRate );
mFileName = filename;
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
@Override
public String getFileName()
{
return mFileName + ".wav";
}
public void start() throws IOException
{
if( !mRunning )
{
mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat );
mWriter.start();
mBufferProcessor = new BufferProcessor();
mBufferProcessor.start();
mRunning = true;
}
}
public void stop() throws IOException
{
if( mRunning )
{
mBufferProcessor = null;
mWriter.stop();
mWriter = null;
mRunning = false;
}
}
public void pause()
{
mPaused = true;
}
public void resume()
{
mPaused = false;
}
/**
* Assumes that the byte[] represents samples corresponding to the
* audio format object defined at construction
*/
public void receive( ByteBuffer buffer )
{
if( mRunning && !mPaused )
{
boolean success = mToWriteBufferQueue.offer( buffer );
if( !success )
{
mLog.error( "Wave recorder buffer overflow - throwing away " +
buffer.capacity() + " samples" );
}
}
}
@Override
public void receive( RealBuffer buffer )
{
//Dispatch the buffer to be written to disk
if( !mToWriteBufferQueue.offer( convert( buffer ) ) )
{
//We had an overflow ... log it
mLog.error( "buffer overflow - throwing away a buffer of sample data" );
}
if( mListener != null )
{
mListener.receive( buffer );
}
}
/**
* Converts the float samples in a complex buffer to a little endian 16-bit
* buffer
*/
public static ByteBuffer convert( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 );
converted.order( ByteOrder.LITTLE_ENDIAN );
for( float sample: samples )
{
converted.putShort( (short)( sample * Short.MAX_VALUE ) );
}
return converted;
}
@Override
public void receive( float sample )
{
mCurrentBuffer.put( sample );
if( !mCurrentBuffer.hasRemaining() )
{
//Dispatch the buffer to be written to disk
if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) )
{
//We had an overflow ... log it
mLog.error( "FloatWaveRecorder: buffer overflow - throwing "
+ "away 1 second of sample data" );
}
mCurrentBuffer.reset();
}
}
public class BufferProcessor extends Thread
{
public void run()
{
while( true )
{
try
{
if( mWriter != null )
{
mWriter.write( mToWriteBufferQueue.take() );
}
}
catch ( IOException ioe )
{
mLog.error( "IOException while trying to write to the wave "
+ "writer", ioe );
}
catch ( InterruptedException e )
{
mLog.error( "Oops ... error while processing the buffer "
+ "queue for the wave recorder", e );
}
}
}
}
}

View File

@@ -0,0 +1,237 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.sound.sampled.AudioFormat;
import module.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.real.IRealBufferListener;
import sample.real.RealBuffer;
import util.TimeStamp;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/**
* WAVE audio recorder module for recording real sample buffers to a wave file
*/
public class RealBufferWaveRecorder extends Module
implements IRealBufferListener, Listener<RealBuffer>
{
private final static Logger mLog =
LoggerFactory.getLogger( RealBufferWaveRecorder.class );
private WaveWriter mWriter;
private String mFilePrefix;
private Path mFile;
private AudioFormat mAudioFormat;
private ThreadPoolManager mThreadPoolManager;
private BufferProcessor mBufferProcessor;
private ScheduledFuture<?> mProcessorHandle;
private LinkedBlockingQueue<RealBuffer> mBuffers =
new LinkedBlockingQueue<>( 500 );
private long mLastBufferReceived;
public RealBufferWaveRecorder( ThreadPoolManager threadPoolManager,
int sampleRate, String filePrefix )
{
mThreadPoolManager = threadPoolManager;
mAudioFormat = new AudioFormat( sampleRate, //SampleRate
16, //Sample Size
1, //Channels
true, //Signed
false ); //Little Endian
mFilePrefix = filePrefix;
}
/**
* Timestamp of when the latest buffer was received by this recorder
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
}
/**
* Converts the float samples in a complex buffer to a little endian 16-bit
* buffer
*/
public static ByteBuffer convert( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 );
converted.order( ByteOrder.LITTLE_ENDIAN );
for( float sample: samples )
{
converted.putShort( (short)( sample * Short.MAX_VALUE ) );
}
return converted;
}
public Path getFile()
{
return mFile;
}
public void start()
{
if( !running() )
{
if( mBufferProcessor == null )
{
mBufferProcessor = new BufferProcessor();
}
try
{
StringBuilder sb = new StringBuilder();
sb.append( mFilePrefix );
sb.append( "_" );
sb.append( TimeStamp.getTimeStamp( "_" ) );
sb.append( ".wav" );
mFile = Paths.get( sb.toString() );
mWriter = new WaveWriter( mAudioFormat, mFile );
/* Schedule the processor to run every 500 milliseconds */
mProcessorHandle = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_RECORDING, mBufferProcessor, 500,
TimeUnit.MILLISECONDS );
}
catch( IOException io )
{
mLog.error( "Error starting real buffer recorder", io );
}
}
}
public boolean running()
{
return mProcessorHandle != null;
}
public void stop()
{
if( running() )
{
mThreadPoolManager.cancel( mProcessorHandle );
mProcessorHandle = null;
if( mWriter != null )
{
try
{
mWriter.close();
}
catch( IOException io )
{
mLog.error( "Error stopping real wave recorder [" +
getFile() + "]", io );
}
}
}
mBuffers.clear();
mWriter = null;
mFile = null;
}
@Override
public void receive( RealBuffer buffer )
{
if( running() )
{
boolean success = mBuffers.offer( buffer );
if( !success )
{
mLog.error( "recorder buffer overflow or error - throwing "
+ "away a buffer of sample data" );
stop();
}
mLastBufferReceived = System.currentTimeMillis();
}
}
@Override
public Listener<RealBuffer> getRealBufferListener()
{
return this;
}
@Override
public void dispose()
{
stop();
mThreadPoolManager = null;
}
@Override
public void reset()
{
}
public class BufferProcessor implements Runnable
{
public void run()
{
try
{
RealBuffer buffer = mBuffers.poll();
while( buffer != null )
{
mWriter.write( convert( buffer ) );
buffer = mBuffers.poll();
}
}
catch ( IOException ioe )
{
/* Stop this module if/when we get an IO exception */
stop();
mLog.error( "IOException while trying to write to the wave "
+ "writer", ioe );
}
}
}
}

View File

@@ -1,141 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;
import javax.sound.sampled.AudioFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.Recorder;
import record.RecorderType;
import util.waveaudio.WaveWriter;
/**
* Threaded WAVE audio recorder.
*/
public class WaveRecorder extends Recorder
{
private final static Logger mLog =
LoggerFactory.getLogger( WaveRecorder.class );
private AudioFormat mAudioFormat;
private boolean mRunning = false;
private boolean mPaused = false;
private WaveWriter mWriter;
private String mFileName;
private BufferProcessor mBufferProcessor;
//Sample buffer queue ... constrained to 512 sample buffers (arbitrary size)
LinkedBlockingQueue<ByteBuffer> mBufferQueue =
new LinkedBlockingQueue<ByteBuffer>( 512 );
public WaveRecorder( AudioFormat format, String filename )
{
super( RecorderType.AUDIO );
mAudioFormat = format;
mFileName = filename;
}
public void start() throws IOException
{
if( !mRunning )
{
mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat );
mWriter.start();
mBufferProcessor = new BufferProcessor();
mBufferProcessor.start();
mRunning = true;
}
}
public void stop() throws IOException
{
if( mRunning )
{
mBufferProcessor = null;
mWriter.stop();
mWriter = null;
mRunning = false;
}
}
public void pause()
{
mPaused = true;
}
public void resume()
{
mPaused = false;
}
/**
* Assumes that the byte[] represents samples corresponding to the
* audio format object defined at construction
*/
public void receive( ByteBuffer buffer )
{
if( mRunning && !mPaused )
{
boolean success = mBufferQueue.offer( buffer );
if( !success )
{
mLog.error( "Wave recorder buffer overflow - throwing away " +
buffer.capacity() + " samples" );
}
}
}
public class BufferProcessor extends Thread
{
public void run()
{
while( true )
{
try
{
if( mWriter != null )
{
mWriter.write( mBufferQueue.take() );
}
}
catch ( IOException ioe )
{
mLog.error( "IOException while trying to write to the "
+ "wave writer", ioe );
}
catch ( InterruptedException e )
{
mLog.error( "Oops ... error while processing the buffer"
+ " queue for the wave recorder", e );
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.sound.sampled.AudioFormat;
public class WaveUtils
{
/**
* Creates a wave file header with the RIFF chunk descriptor and the WAVE
* format chunk 1 and chunk 2 header
*/
public static ByteBuffer getWaveHeader( AudioFormat format )
{
ByteBuffer descriptor = ByteBuffer.allocate( 44 );
descriptor.order( ByteOrder.LITTLE_ENDIAN );
/* Chunk ID = RIFF */
descriptor.put( (byte)0x52 ); //R
descriptor.put( (byte)0x49 ); //I
descriptor.put( (byte)0x46 ); //F
descriptor.put( (byte)0x46 ); //F
/* Chunk Size = 36 bytes (initial empty size) */
descriptor.put( (byte)36 );
descriptor.put( (byte)0 );
descriptor.put( (byte)0 );
descriptor.put( (byte)0 );
/* Format = WAVE */
descriptor.put( (byte)0x57 ); //W
descriptor.put( (byte)0x41 ); //A
descriptor.put( (byte)0x56 ); //V
descriptor.put( (byte)0x45 ); //E
/* Sub Chunk 1 ID */
descriptor.put( (byte)0x66 ); //f
descriptor.put( (byte)0x6D ); //m
descriptor.put( (byte)0x74 ); //t
descriptor.put( (byte)0x20 ); //space
/* SubChunk1Size = 1 */
descriptor.put( (byte)0x10 );
descriptor.put( (byte)0x0 );
descriptor.put( (byte)0x0 );
descriptor.put( (byte)0x0 );
/* Audio format = 1 (Uncompressed PCM) */
descriptor.put( (byte)0x1 );
descriptor.put( (byte)0x0 );
/* Number of Channels */
descriptor.asShortBuffer().put( (short)format.getChannels() );
descriptor.position( descriptor.position() + 2 );
/* Sample Rate - assumes integral sample rate */
descriptor.asIntBuffer().put( (int)format.getSampleRate() );
descriptor.position( descriptor.position() + 4 );
int frameByteRate = format.getChannels() * format.getSampleSizeInBits() / 8;
/* Byte Rate = sample rate * channels * bits per sample / 8 */
int byteRate = (int)( format.getSampleRate() * frameByteRate );
descriptor.asIntBuffer().put( byteRate );
descriptor.position( descriptor.position() + 4 );
/* Block Align */
descriptor.asShortBuffer().put( (short)frameByteRate );
descriptor.position( descriptor.position() + 2 );
/* Bits per Sample */
descriptor.asShortBuffer().put( (short)( format.getSampleSizeInBits() ) );
descriptor.position( descriptor.position() + 2 );
/* Sub Chunk 2 ID */
descriptor.put( (byte)0x64 ); //d
descriptor.put( (byte)0x61 ); //a
descriptor.put( (byte)0x74 ); //t
descriptor.put( (byte)0x61 ); //a
/* Sub Chunk 2 Size */
descriptor.put( (byte)0x0 );
descriptor.put( (byte)0x0 );
descriptor.put( (byte)0x0 );
descriptor.put( (byte)0x0 );
return descriptor;
}
}

View File

@@ -0,0 +1,251 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WaveWriter implements AutoCloseable
{
private final static Logger mLog = LoggerFactory.getLogger( WaveWriter.class );
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
{
assert( format != null );
assert( 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
{
Files.createFile( mFile );
mFileChannel = (FileChannel.open( mFile, StandardOpenOption.WRITE ) );
ByteBuffer header = WaveUtils.getWaveHeader( mAudioFormat );
header.flip();
while( header.hasRemaining() )
{
mFileChannel.write( header );
}
}
/**
* Closes the file
*/
public void close() throws IOException
{
mFileChannel.close();
}
/**
* 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) */
mFileChannel.write( getUnsignedIntegerBuffer( mFileChannel.size() - 8 ), 4 );
/* Update chunk 2 size (total size - 44 bytes) */
mFileChannel.write( getUnsignedIntegerBuffer( mFileChannel.size() - 44 ), 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.
*/
private static ByteBuffer getUnsignedIntegerBuffer( long size )
{
ByteBuffer buffer = ByteBuffer.allocate( 4 );
buffer.put( (byte)( size & 0xFFl ) );
buffer.put( (byte)( ( size & 0xFF00l ) >> 8 ) );
buffer.put( (byte)( ( size & 0xFF0000l ) >> 16 ) );
buffer.put( (byte)( ( size & 0xFF000000l ) >> 32 ) );
buffer.position( 0 );
return buffer;
}
/**
* 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 );
}
}

View File

@@ -1,72 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 java.io.File;
import util.waveaudio.WavFile;
public class WriteExample
{
public static void main(String[] args)
{
try
{
int sampleRate = 44100; // Samples per second
double duration = 5.0; // Seconds
// Calculate the number of frames required for specified duration
long numFrames = (long)(duration * sampleRate);
// Create a wav file with the name specified as the first argument
WavFile wavFile = WavFile.newWavFile(new File(args[0]), 2, numFrames, 16, sampleRate);
// Create a buffer of 100 frames
double[][] buffer = new double[2][100];
// Initialise a local frame counter
long frameCounter = 0;
// Loop until all frames written
while (frameCounter < numFrames)
{
// Determine how many frames to write, up to a maximum of the buffer size
long remaining = wavFile.getFramesRemaining();
int toWrite = (remaining > 100) ? 100 : (int) remaining;
// Fill the buffer, one tone per channel
for (int s=0 ; s<toWrite ; s++, frameCounter++)
{
buffer[0][s] = Math.sin(2.0 * Math.PI * 400 * frameCounter / sampleRate);
buffer[1][s] = Math.sin(2.0 * Math.PI * 500 * frameCounter / sampleRate);
}
// Write the buffer
wavFile.writeFrames(buffer, toWrite);
}
// Close the wavFile
wavFile.close();
}
catch (Exception e)
{
System.err.println(e);
}
}
}