[WIP] Updates the sample adapters to natively use ReusableBuffers.

Refactors a MixerReader instance from the RealMixerSource so that it
can be reused for both real and complex data outputs.

Updates RealMixerSource to use refactored MixerReader and use a default
sample rate of 8 kHz.

Updates the ComplexMixer to use the refactored MixerReader.

Updates the FCD 1/2 tuners for compatibility with the new ComplexMixer.

Removes several unused classes.
This commit is contained in:
Denny
2018-06-24 09:50:36 -04:00
parent 19fa256c60
commit a621a31aa9
19 changed files with 672 additions and 760 deletions

View File

@@ -16,69 +16,74 @@
package io.github.dsheirer.source.mixer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.adapter.IComplexSampleAdapter;
import io.github.dsheirer.sample.adapter.ComplexSampleAdapter;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import java.util.concurrent.atomic.AtomicBoolean;
public class ComplexMixer
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexMixer.class);
private long mFrequency = 0;
private BufferReader mBufferReader = new BufferReader();
private TargetDataLine mTargetDataLine;
private AudioFormat mAudioFormat;
private MixerReader<ReusableComplexBuffer> mMixerReader;
private String mName;
private int mBytesPerFrame = 0;
private IComplexSampleAdapter mSampleAdapter;
private Listener<ReusableComplexBuffer> mListener;
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ComplexMixer");
/**
* Complex Mixer - constructs a reader on the mixer/sound card target data line using the specified audio
* format (sample size, sample rate ) and broadcasts complex (I/Q) sample buffers to all registered listeners.
* Reads sample buffers sized to 10% of the sample rate specified in audio format.
* format (sample size, sample rate ) and broadcasts complex (I/Q) sample buffers to the registered listener.
*
* @param targetDataLine - mixer or sound card to be used
* @param format - audio format
* @param name - token name to use for this mixer
* @param complexSampleAdapter - adapter to convert byte array data read from the mixer into ComplexBuffer.
* @param listener to receive generated complex buffers
*/
public ComplexMixer(TargetDataLine targetDataLine, AudioFormat format, String name,
IComplexSampleAdapter complexSampleAdapter, Listener<ReusableComplexBuffer> listener)
ComplexSampleAdapter complexSampleAdapter)
{
mTargetDataLine = targetDataLine;
mMixerReader = new MixerReader<>(format, targetDataLine, complexSampleAdapter);
mName = name;
mAudioFormat = format;
mSampleAdapter = complexSampleAdapter;
mListener = listener;
}
/**
* Sets the buffer size in bytes per buffer for each read interval.
* @param bufferSize in bytes
*/
public void setBufferSize(int bufferSize)
{
mMixerReader.setBufferSize(bufferSize);
}
public void start()
{
if(!mBufferReader.isRunning())
{
Thread thread = new Thread(mBufferReader);
thread.setDaemon(true);
thread.setName(mName + " Complex Sample Reader");
thread.start();
}
mMixerReader.start();
}
public void stop()
{
mBufferReader.stop();
mMixerReader.stop();
}
public void setBufferListener(Listener<ReusableComplexBuffer> listener)
{
mMixerReader.setBufferListener(listener);
}
public void removeBufferListener()
{
mMixerReader.removeBufferListener();
}
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mMixerReader.setSourceEventListener(listener);
}
public void removeSourceEventListener()
{
mMixerReader.removeSourceEventListener();
}
@Override
@@ -87,144 +92,8 @@ public class ComplexMixer
return mName;
}
public int getSampleRate()
{
if(mTargetDataLine != null)
{
return (int)mTargetDataLine.getFormat().getSampleRate();
}
else
{
return 0;
}
}
/**
* Returns the frequency of this source. Default is 0.
*/
public long getFrequency() throws SourceException
{
return mFrequency;
}
/**
* Specify the frequency that will be returned from this source. This may
* be useful if you are streaming an external audio source in through the
* sound card and you want to specify a frequency for that source
*/
public void setFrequency(long frequency)
{
mFrequency = frequency;
}
/**
* Reader thread. Performs blocking read against the mixer target data
* line, converts the samples to an array of floats using the specified
* adapter. Dispatches float arrays to all registered listeners.
*/
public class BufferReader implements Runnable
{
private AtomicBoolean mRunning = new AtomicBoolean();
@Override
public void run()
{
if(mRunning.compareAndSet(false, true))
{
if(mTargetDataLine == null)
{
mRunning.set(false);
mLog.error("ComplexMixerSource - mixer target data line is null");
}
else
{
mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8;
if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED)
{
mBytesPerFrame = 2;
}
//Set buffer size to 1/10 second of samples
int sampleCount = (int)(mAudioFormat.getSampleRate() * 0.10) * 2;
int bufferSize = sampleCount * mBytesPerFrame;
//We'll reuse the same buffer for each read
byte[] buffer = new byte[bufferSize];
try
{
mTargetDataLine.open(mAudioFormat);
mTargetDataLine.start();
}
catch(LineUnavailableException e)
{
mLog.error("ComplexMixerSource - mixer target data line not available to read data from", e);
mRunning.set(false);
}
while(mRunning.get())
{
try
{
//Blocking read - waits until the buffer fills
mTargetDataLine.read(buffer, 0, buffer.length);
/* Dispatch samples to registered listeners */
if(mListener != null)
{
ReusableComplexBuffer reusableComplexBuffer = mReusableComplexBufferQueue.getBuffer(sampleCount);
//Convert samples and load into reusable buffer
mSampleAdapter.convertAndLoad(buffer, reusableComplexBuffer);
mListener.receive(reusableComplexBuffer);
}
}
catch(Exception e)
{
mLog.error("error while reading from the mixer target data line", e);
mRunning.set(false);
}
}
}
/* Close the data line if it is still open */
if(mTargetDataLine != null && mTargetDataLine.isOpen())
{
mTargetDataLine.close();
}
}
}
/**
* Stops the reader thread
*/
public void stop()
{
if(mRunning.compareAndSet(true, false))
{
mTargetDataLine.stop();
mTargetDataLine.close();
}
}
/**
* Indicates if the reader thread is running
*/
public boolean isRunning()
{
return mRunning.get();
}
}
public void dispose()
{
if(mBufferReader != null)
{
mBufferReader.stop();
}
mListener = null;
mMixerReader.dispose();
}
}

View File

@@ -16,8 +16,8 @@
package io.github.dsheirer.source.mixer;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.sample.adapter.ChannelShortAdapter;
import io.github.dsheirer.sample.adapter.ShortAdapter;
import io.github.dsheirer.sample.adapter.RealChannelShortAdapter;
import io.github.dsheirer.sample.adapter.RealShortAdapter;
import io.github.dsheirer.source.config.SourceConfigMixer;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.MixerTunerDataLine;
@@ -90,7 +90,7 @@ public class MixerManager
if(dataLine != null)
{
return new RealMixerSource(dataLine, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
new ShortAdapter());
new RealShortAdapter("RealMixerSource - Mono"));
}
}
catch(LineUnavailableException e)
@@ -100,7 +100,7 @@ public class MixerManager
}
}
else
else //STEREO
{
DataLine.Info info = new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO);
@@ -114,7 +114,7 @@ public class MixerManager
if(dataLine != null)
{
return new RealMixerSource(dataLine, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO,
new ChannelShortAdapter(mixerConfig.getChannel()));
new RealChannelShortAdapter(mixerConfig.getChannel(), "RealMixerSource-Stereo"));
}
}
catch(LineUnavailableException e)

View File

@@ -0,0 +1,283 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.source.mixer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.adapter.AbstractSampleAdapter;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import java.util.Arrays;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Target Data Line sample data processor. Performs blocking read against the mixer target data line, converts
* the samples to an array of floats using the specified adapter. Dispatches float arrays to the registered
* buffer listener.
*/
public class MixerReader<T extends ReusableBuffer> implements Runnable
{
private final static Logger mLog = LoggerFactory.getLogger(MixerReader.class);
private static final long BUFFER_PROCESSING_INTERVAL_MS = 50; //50 milliseconds, 20 reads per second
private TargetDataLine mTargetDataLine;
private byte[] mBuffer;
private int mBufferSize;
private AtomicBoolean mRunning = new AtomicBoolean();
private ScheduledFuture mScheduledFuture;
private int mBytesRead;
private AbstractSampleAdapter<T> mSampleAdapter;
private Listener<T> mReusableBufferListener;
private Listener<SourceEvent> mSourceEventListener;
private AudioFormat mAudioFormat;
private HeartbeatManager mHeartbeatManager;
public MixerReader(AudioFormat audioFormat, TargetDataLine targetDataLine,
AbstractSampleAdapter<T> abstractSampleAdapter, HeartbeatManager heartbeatManager)
{
mTargetDataLine = targetDataLine;
mAudioFormat = audioFormat;
mSampleAdapter = abstractSampleAdapter;
mHeartbeatManager = heartbeatManager;
/* Set buffer size to 1/10 second of samples */
mBufferSize = (int)(mAudioFormat.getSampleRate() * 0.1) * mAudioFormat.getFrameSize();
}
public MixerReader(AudioFormat audioFormat, TargetDataLine targetDataLine,
AbstractSampleAdapter<T> abstractSampleAdapter)
{
this(audioFormat, targetDataLine, abstractSampleAdapter, null);
}
/**
* Sets the size of buffers to use. This reader will read from target data line 20 times per second.
* @param bytesPerBuffer to size for each buffer read.
*/
public void setBufferSize(int bytesPerBuffer)
{
mBufferSize = bytesPerBuffer;
mBuffer = new byte[mBufferSize];
}
/**
* Opens the source mixer target data line
*
* @throws LineUnavailableException if the target data line is null or unavailable (from the OS).
*/
private void openTargetDataLine() throws LineUnavailableException
{
if(mTargetDataLine == null)
{
throw new LineUnavailableException("Source Mixer TargetDataLine is null");
}
mBuffer = new byte[mBufferSize];
mTargetDataLine.open(mAudioFormat);
mTargetDataLine.start();
}
/**
* Closes the mixer source target data line
*/
private void closeTargetDataLine()
{
if(mTargetDataLine != null && mTargetDataLine.isOpen())
{
mTargetDataLine.close();
}
}
/**
* Starts the sample processing reader thread
*/
public void start()
{
if(mRunning.compareAndSet(false, true))
{
try
{
openTargetDataLine();
}
catch(LineUnavailableException e)
{
mLog.error("Unable to open mixer source target data line", e);
mRunning.set(false);
return;
}
//Cancel the scheduled thread if it wasn't cancelled previously ... this shouldn't happen
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
mScheduledFuture = null;
}
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this,
0, BUFFER_PROCESSING_INTERVAL_MS, TimeUnit.MILLISECONDS);
}
else
{
mLog.warn("Attempt to start an already running BufferReader instance - this shouldn't happen");
}
}
/**
* Stops the sample processing reader thread
*/
public void stop()
{
if(mRunning.compareAndSet(true, false))
{
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
mScheduledFuture = null;
}
closeTargetDataLine();
}
else
{
mLog.warn("Attempt to stop an already stopped BufferReader instance - this shouldn't happen");
}
}
/**
* Sample processing thread. This method runs each time the timer fires.
*/
@Override
public void run()
{
if(mRunning.get())
{
if(mHeartbeatManager != null)
{
mHeartbeatManager.broadcast();
}
try
{
mBytesRead = 0;
//Blocking read - waits until the buffer fills
mBytesRead = mTargetDataLine.read(mBuffer, 0, mBuffer.length);
if(mBytesRead == mBuffer.length)
{
//Sample adapter automatically sets initial listener count to one.
T reusableBuffer = mSampleAdapter.convert(mBuffer);
if(reusableBuffer != null && mReusableBufferListener != null)
{
mReusableBufferListener.receive(reusableBuffer);
}
}
else if(mBytesRead > 0)
{
//Sample adapter automatically sets initial listener count to one.
T reusableBuffer = mSampleAdapter.convert(Arrays.copyOf(mBuffer, mBytesRead));
if(reusableBuffer != null && mReusableBufferListener != null)
{
mReusableBufferListener.receive(reusableBuffer);
}
}
}
catch(Exception e)
{
mLog.error("MixerSource - error while reading from the mixer target data line", e);
stop();
}
}
}
/**
* Indicates if the reader thread is running
*/
public boolean isRunning()
{
return mRunning.get();
}
/**
* Sets the listener to receive sample data in reusable buffers.
*/
public void setBufferListener(Listener<T> listener)
{
mReusableBufferListener = listener;
}
/**
* Removes the listener from receiving sample data.
*/
public void removeBufferListener()
{
mReusableBufferListener = null;
}
/**
* Registers a listener to receive source events
*
* @param listener
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventListener = listener;
}
/**
* Removes a listener from receiving source events
*/
public void removeSourceEventListener()
{
mSourceEventListener = null;
}
/**
* Current source event listener
*/
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventListener;
}
/**
* Sample rate specified for the underlying target data line.
*/
public double getSampleRate()
{
return mAudioFormat.getSampleRate();
}
public void dispose()
{
stop();
mHeartbeatManager = null;
mReusableBufferListener = null;
mSourceEventListener = null;
}
}

View File

@@ -16,38 +16,18 @@
package io.github.dsheirer.source.mixer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.adapter.ISampleAdapter;
import io.github.dsheirer.sample.adapter.AbstractSampleAdapter;
import io.github.dsheirer.sample.buffer.ReusableBuffer;
import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.source.RealSource;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
public class RealMixerSource extends RealSource
{
private final static Logger mLog = LoggerFactory.getLogger(RealMixerSource.class);
private ReusableBufferBroadcaster<ReusableBuffer> mBufferBroadcaster = new ReusableBufferBroadcaster<>();
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealMixerSource");
private long mFrequency = 0;
private int mBufferSize = 16384;
private BufferReader mBufferReader = new BufferReader();
private TargetDataLine mTargetDataLine;
private AudioFormat mAudioFormat;
private int mBytesPerFrame = 0;
private ISampleAdapter mSampleAdapter;
private CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>();
private long mFrequency;
private MixerReader mMixerReader;
/**
* Real Mixer Source - constructs a reader on the mixer/sound card target
@@ -58,89 +38,94 @@ public class RealMixerSource extends RealSource
* @param targetDataLine - mixer or sound card to be used
* @param format - audio format
* @param sampleAdapter - adapter to convert byte array data read from the
* mixer into float array data. Can optionally invert the channel data if
* the left/right stereo channels are inverted.
* mixer into float array data.
*/
public RealMixerSource(TargetDataLine targetDataLine, AudioFormat format, ISampleAdapter sampleAdapter)
public RealMixerSource(TargetDataLine targetDataLine, AudioFormat format, AbstractSampleAdapter sampleAdapter)
{
mTargetDataLine = targetDataLine;
mAudioFormat = format;
mSampleAdapter = sampleAdapter;
mMixerReader = new MixerReader(format, targetDataLine, sampleAdapter, getHeartbeatManager());
}
/**
* Registers a listener to receive source events
*
* @param listener
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
//Not implemented
mMixerReader.setSourceEventListener(listener);
}
/**
* Removes a listener from receiving source events
*/
@Override
public void removeSourceEventListener()
{
//Not implemented
mMixerReader.removeSourceEventListener();
}
/**
* Current source event listener
*/
@Override
public Listener<SourceEvent> getSourceEventListener()
{
//Not implemented
return null;
return mMixerReader.getSourceEventListener();
}
@Override
public void reset()
{
stop();
}
@Override
public void start()
{
if(!mMixerReader.isRunning())
{
mMixerReader.start();
}
}
@Override
public void stop()
{
}
public void setListener(Listener<ReusableBuffer> listener)
{
mBufferBroadcaster.addListener(listener);
/* If this is the first listener, start the reader thread */
if(!mBufferReader.isRunning())
if(mMixerReader.isRunning())
{
Thread thread = new Thread(mBufferReader);
thread.setDaemon(true);
thread.setName("Sample Reader");
thread.start();
}
}
public void removeListener(Listener<ReusableBuffer> listener)
{
mBufferBroadcaster.removeListener(listener);
/* If this is the last listener, stop the reader thread */
if(mSampleListeners.isEmpty())
{
mBufferReader.stop();
}
}
public double getSampleRate()
{
if(mTargetDataLine != null)
{
return mTargetDataLine.getFormat().getSampleRate();
}
else
{
return 0;
mMixerReader.stop();
}
}
/**
* Returns the frequency of this source. Default is 0.
* Sets the listener to receive sample data in reusable buffers.
*/
public void setListener(Listener<ReusableBuffer> listener)
{
mMixerReader.setBufferListener(listener);
}
/**
* Removes the listener from receiving sample data.
* @param listener
*/
public void removeListener(Listener<ReusableBuffer> listener)
{
mMixerReader.removeBufferListener();
}
/**
* Sample rate specified for the underlying target data line.
*/
@Override
public double getSampleRate()
{
return mMixerReader.getSampleRate();
}
/**
* Returns the frequency of this source. Default is 0 if the frequency hasn't been set.
*/
public long getFrequency()
{
@@ -148,131 +133,18 @@ public class RealMixerSource extends RealSource
}
/**
* Specify the frequency that will be returned from this source. This may
* be useful if you are streaming an external audio source in through the
* sound card and you want to specify a frequency for that source
* Specify the frequency that will be returned from this source. This may be useful if you are streaming an
* external audio source in through the sound card and you want to specify a frequency for that source
*/
public void setFrequency(long frequency)
{
mFrequency = frequency;
}
/**
* Reader thread. Performs blocking read against the mixer target data
* line, converts the samples to an array of floats using the specified
* adapter. Dispatches float arrays to all registered listeners.
*/
public class BufferReader implements Runnable
{
private AtomicBoolean mRunning = new AtomicBoolean();
@Override
public void run()
{
if(mRunning.compareAndSet(false, true))
{
getHeartbeatManager().broadcast();
if(mTargetDataLine == null)
{
mRunning.set(false);
mLog.error("ComplexMixerSource - mixer target data line"
+ " is null");
}
else
{
mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8;
if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED)
{
mBytesPerFrame = 2;
}
/* Set buffer size to 1/10 second of samples */
mBufferSize = (int)(mAudioFormat.getSampleRate()
* 0.05) * mBytesPerFrame;
/* We'll reuse the same buffer for each read */
byte[] buffer = new byte[mBufferSize];
try
{
mTargetDataLine.open(mAudioFormat);
mTargetDataLine.start();
}
catch(LineUnavailableException e)
{
mLog.error("ComplexMixerSource - mixer target data line"
+ "not available to read data from", e);
mRunning.set(false);
}
while(mRunning.get())
{
try
{
/* Blocking read - waits until the buffer fills */
mTargetDataLine.read(buffer, 0, buffer.length);
/* Convert samples to float array */
//TODO: change this sample adapter over to use reusable buffers and move the buffer queue into the adapter
float[] samples = mSampleAdapter.convert(buffer);
/* Dispatch samples to registered listeners */
ReusableBuffer reusableBuffer = mReusableBufferQueue.getBuffer(samples.length);
reusableBuffer.reloadFrom(samples, System.currentTimeMillis());
reusableBuffer.incrementUserCount();
mBufferBroadcaster.broadcast(reusableBuffer);
}
catch(Exception e)
{
mLog.error("ComplexMixerSource - error while reading"
+ "from the mixer target data line", e);
mRunning.set(false);
}
}
}
/* Close the data line if it is still open */
if(mTargetDataLine != null && mTargetDataLine.isOpen())
{
mTargetDataLine.close();
}
}
}
/**
* Stops the reader thread
*/
public void stop()
{
if(mRunning.compareAndSet(true, false))
{
mTargetDataLine.stop();
mTargetDataLine.close();
}
}
/**
* Indicates if the reader thread is running
*/
public boolean isRunning()
{
return mRunning.get();
}
}
@Override
public void dispose()
{
if(mBufferReader != null)
{
mBufferReader.stop();
mSampleListeners.clear();
}
mMixerReader.dispose();
}
}