mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
Interim checkin for call recording.
This commit is contained in:
@@ -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 ) );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
225
src/record/wave/ComplexBufferWaveRecorder.java
Normal file
225
src/record/wave/ComplexBufferWaveRecorder.java
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/record/wave/RealBufferWaveRecorder.java
Normal file
237
src/record/wave/RealBufferWaveRecorder.java
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/record/wave/WaveUtils.java
Normal file
108
src/record/wave/WaveUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
251
src/record/wave/WaveWriter.java
Normal file
251
src/record/wave/WaveWriter.java
Normal 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 );
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user