mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
[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:
@@ -15,14 +15,9 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
|
||||
public interface IComplexSampleAdapter
|
||||
public abstract class AbstractSampleAdapter<T extends ReusableBuffer>
|
||||
{
|
||||
/**
|
||||
* Converts the sample byte array into complex float samples, loading the converted float samples into the buffer.
|
||||
* @param samples
|
||||
* @param reusableComplexBuffer
|
||||
*/
|
||||
void convertAndLoad(byte[] samples, ReusableComplexBuffer reusableComplexBuffer);
|
||||
public abstract T convert(byte[] sampleBytes);
|
||||
}
|
||||
@@ -1,52 +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 io.github.dsheirer.sample.adapter;
|
||||
|
||||
public class ByteSampleAdapter implements ISampleAdapter
|
||||
{
|
||||
private final static float[] LOOKUP_VALUES;
|
||||
|
||||
/**
|
||||
* Creates a static lookup table that converts the 8-bit valued range
|
||||
* from 0 - 255 into scaled float values of -1.0 to 0 to 1.0
|
||||
*/
|
||||
static
|
||||
{
|
||||
LOOKUP_VALUES = new float[ 256 ];
|
||||
|
||||
for( int x = 0; x < 256; x++ )
|
||||
{
|
||||
LOOKUP_VALUES[ x ] = (float)( x - 127 ) / 128.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] convert( byte[] samples )
|
||||
{
|
||||
float[] convertedSamples = new float[ samples.length ];
|
||||
int pointer = 0;
|
||||
|
||||
for( byte sample: samples )
|
||||
{
|
||||
/* Convert byte value into float from the lookup table */
|
||||
convertedSamples[ pointer++ ] = LOOKUP_VALUES[ ( sample & 0xFF ) ];
|
||||
}
|
||||
|
||||
return convertedSamples;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +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 io.github.dsheirer.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Converts 16-bit little endian byte data into an array of floats
|
||||
*/
|
||||
public class ChannelShortAdapter implements ISampleAdapter
|
||||
{
|
||||
private ShortToFloatMap mMap = new ShortToFloatMap();
|
||||
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;
|
||||
private MixerChannel mMixerChannel;
|
||||
|
||||
public ChannelShortAdapter( MixerChannel channel )
|
||||
{
|
||||
/* Only use with LEFT/RIGHT channels */
|
||||
Validate.isTrue(channel != MixerChannel.MONO);
|
||||
|
||||
mMixerChannel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] convert( byte[] samples )
|
||||
{
|
||||
float[] processed = new float[ samples.length / 4 ];
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
/* Wrap byte array in a byte buffer so we can process them as shorts */
|
||||
ByteBuffer buffer = ByteBuffer.wrap( samples );
|
||||
|
||||
/* Set endian to correct byte ordering */
|
||||
buffer.order( mByteOrder );
|
||||
|
||||
while( buffer.hasRemaining() )
|
||||
{
|
||||
if( mMixerChannel == MixerChannel.LEFT )
|
||||
{
|
||||
processed[ pointer ] = mMap.get( buffer.getShort() );
|
||||
|
||||
/* Throw away the right channel */
|
||||
buffer.getShort();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Throw away the left channel */
|
||||
buffer.getShort();
|
||||
|
||||
processed[ pointer ] = mMap.get( buffer.getShort() );
|
||||
}
|
||||
|
||||
pointer++;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set byte interpretation to little or big endian. Defaults to LITTLE
|
||||
* endian
|
||||
*/
|
||||
public void setByteOrder( ByteOrder order )
|
||||
{
|
||||
mByteOrder = order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
|
||||
public abstract class ComplexSampleAdapter extends AbstractSampleAdapter<ReusableComplexBuffer>
|
||||
{
|
||||
private ReusableComplexBufferQueue mReusableBufferQueue;
|
||||
|
||||
/**
|
||||
* Constructs a real sample adapter
|
||||
*
|
||||
* @param debugName to use for debug logging
|
||||
*/
|
||||
public ComplexSampleAdapter(String debugName)
|
||||
{
|
||||
mReusableBufferQueue = new ReusableComplexBufferQueue(debugName);
|
||||
}
|
||||
|
||||
protected ReusableComplexBuffer getBuffer(int size)
|
||||
{
|
||||
return mReusableBufferQueue.getBuffer(size);
|
||||
}
|
||||
}
|
||||
@@ -16,43 +16,61 @@
|
||||
package io.github.dsheirer.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
/**
|
||||
* Converts 16-bit little endian byte data into an array of complex floats
|
||||
* Converts 16-bit/2-byte little endian byte data into a reusable buffer of complex float sample data
|
||||
*/
|
||||
public class ComplexShortAdapter implements IComplexSampleAdapter
|
||||
public class ComplexShortAdapter extends ComplexSampleAdapter
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexShortAdapter.class);
|
||||
private ShortToFloatMap mMap = new ShortToFloatMap();
|
||||
private FloatBuffer mFloatBuffer;
|
||||
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;
|
||||
private ByteBuffer mByteBuffer;
|
||||
|
||||
/**
|
||||
* Constructs a real sample adapter
|
||||
*
|
||||
* @param debugName to use for debug logging
|
||||
*/
|
||||
public ComplexShortAdapter(String debugName)
|
||||
{
|
||||
super(debugName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndLoad(byte[] samples, ReusableComplexBuffer reusableComplexBuffer)
|
||||
public ReusableComplexBuffer convert(byte[] samples)
|
||||
{
|
||||
int sampleCount = samples.length / 2;
|
||||
ReusableComplexBuffer reusableBuffer = getBuffer(samples.length / 2);
|
||||
float[] convertedSamples = reusableBuffer.getSamples();
|
||||
|
||||
if(mFloatBuffer == null || mFloatBuffer.capacity() != sampleCount)
|
||||
{
|
||||
mFloatBuffer = FloatBuffer.allocate(sampleCount);
|
||||
}
|
||||
int pointer = 0;
|
||||
|
||||
mFloatBuffer.rewind();
|
||||
|
||||
/* Wrap byte array in a byte buffer so we can process them as shorts */
|
||||
ByteBuffer buffer = ByteBuffer.wrap(samples);
|
||||
mByteBuffer = ByteBuffer.wrap(samples);
|
||||
|
||||
/* Set endian to correct byte ordering */
|
||||
buffer.order(mByteOrder);
|
||||
mByteBuffer.order(mByteOrder);
|
||||
|
||||
while(buffer.hasRemaining())
|
||||
while(mByteBuffer.hasRemaining())
|
||||
{
|
||||
mFloatBuffer.put(mMap.get(buffer.getShort()));
|
||||
convertedSamples[pointer] = mMap.get(mByteBuffer.getShort());
|
||||
pointer++;
|
||||
}
|
||||
|
||||
reusableComplexBuffer.reloadFrom(mFloatBuffer, System.currentTimeMillis());
|
||||
reusableBuffer.incrementUserCount();
|
||||
return reusableBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set byte interpretation to little or big endian. Defaults to LITTLE
|
||||
* endian
|
||||
*/
|
||||
public void setByteOrder(ByteOrder order)
|
||||
{
|
||||
mByteOrder = order;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +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 io.github.dsheirer.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Receives complex samples and send either the left channel or the right
|
||||
* channel to the short sample listener
|
||||
*/
|
||||
public class ComplexToFloatSampleConverter implements Listener<List<Complex>>
|
||||
{
|
||||
private MixerChannel mChannel;
|
||||
private Listener<Float> mListener;
|
||||
|
||||
public ComplexToFloatSampleConverter( Listener<Float> listener,
|
||||
MixerChannel channel )
|
||||
{
|
||||
mListener = listener;
|
||||
mChannel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( List<Complex> samples )
|
||||
{
|
||||
for( Complex sample: samples )
|
||||
{
|
||||
mListener.receive( mChannel == MixerChannel.LEFT ?
|
||||
sample.left() :
|
||||
sample.right() );
|
||||
}
|
||||
}
|
||||
|
||||
public Listener<Float> getListener()
|
||||
{
|
||||
return mListener;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +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 io.github.dsheirer.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class FloatArrayToComplexAdapter implements Listener<Float[]>
|
||||
{
|
||||
private Listener<Complex> mListener;
|
||||
|
||||
@Override
|
||||
public void receive( Float[] samples )
|
||||
{
|
||||
if( samples != null )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x += 2 )
|
||||
{
|
||||
if( mListener != null &&
|
||||
samples[ x ] != null &&
|
||||
samples[ x + 1 ] != null )
|
||||
{
|
||||
mListener.receive( new Complex( samples[ x ],
|
||||
samples[ x + 1 ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
samples = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<Complex> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( Listener<Complex> listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.sample.adapter;
|
||||
|
||||
/*
|
||||
* Adapter to convert byte data received from a data stream into float[]
|
||||
* samples
|
||||
*/
|
||||
public interface ISampleAdapter
|
||||
{
|
||||
public abstract float[] convert( byte[] samples );
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Adapter for converting stereo/2-channel short sample data to single channel float samples.
|
||||
*/
|
||||
public class RealChannelShortAdapter extends RealSampleAdapter
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealChannelShortAdapter.class);
|
||||
private ShortToFloatMap mMap = new ShortToFloatMap();
|
||||
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;
|
||||
private MixerChannel mMixerChannel;
|
||||
private ByteBuffer mByteBuffer;
|
||||
|
||||
public RealChannelShortAdapter(MixerChannel channel, String debugName)
|
||||
{
|
||||
super(debugName);
|
||||
|
||||
/* Only use with LEFT/RIGHT channels */
|
||||
Validate.isTrue(channel != MixerChannel.MONO);
|
||||
|
||||
mMixerChannel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReusableBuffer convert(byte[] samples)
|
||||
{
|
||||
ReusableBuffer reusableBuffer = getBuffer(samples.length / 4);
|
||||
float[] convertedSamples = reusableBuffer.getSamples();
|
||||
|
||||
mByteBuffer = ByteBuffer.wrap(samples);
|
||||
|
||||
/* Set endian to correct byte ordering */
|
||||
mByteBuffer.order(mByteOrder);
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
while(mByteBuffer.hasRemaining())
|
||||
{
|
||||
if(mMixerChannel == MixerChannel.LEFT)
|
||||
{
|
||||
convertedSamples[pointer++] = mMap.get(mByteBuffer.getShort());
|
||||
|
||||
//Throw away the right channel
|
||||
mByteBuffer.getShort();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Throw away the left channel
|
||||
mByteBuffer.getShort();
|
||||
|
||||
convertedSamples[pointer++] = mMap.get(mByteBuffer.getShort());
|
||||
}
|
||||
}
|
||||
|
||||
reusableBuffer.incrementUserCount();
|
||||
return reusableBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set byte interpretation to little or big endian. Defaults to LITTLE
|
||||
* endian
|
||||
*/
|
||||
public void setByteOrder(ByteOrder order)
|
||||
{
|
||||
mByteOrder = order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
|
||||
public abstract class RealSampleAdapter extends AbstractSampleAdapter<ReusableBuffer>
|
||||
{
|
||||
private ReusableBufferQueue mReusableBufferQueue;
|
||||
|
||||
/**
|
||||
* Constructs a real sample adapter
|
||||
*
|
||||
* @param debugName to use for debug logging
|
||||
*/
|
||||
public RealSampleAdapter(String debugName)
|
||||
{
|
||||
mReusableBufferQueue = new ReusableBufferQueue(debugName);
|
||||
}
|
||||
|
||||
protected ReusableBuffer getBuffer(int size)
|
||||
{
|
||||
return mReusableBufferQueue.getBuffer(size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Converts 16-bit/2-byte little endian byte data into a reusable buffer of real float sample data
|
||||
*/
|
||||
public class RealShortAdapter extends RealSampleAdapter
|
||||
{
|
||||
private ByteBuffer mByteBuffer;
|
||||
private ShortToFloatMap mMap = new ShortToFloatMap();
|
||||
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;
|
||||
|
||||
/**
|
||||
* Constructs a real sample adapter
|
||||
*
|
||||
* @param debugName to use for debug logging
|
||||
*/
|
||||
public RealShortAdapter(String debugName)
|
||||
{
|
||||
super(debugName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReusableBuffer convert(byte[] samples)
|
||||
{
|
||||
ReusableBuffer reusableBuffer = getBuffer(samples.length / 2);
|
||||
float[] convertedSamples = reusableBuffer.getSamples();
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
mByteBuffer = ByteBuffer.wrap(samples);
|
||||
|
||||
/* Set endian to correct byte ordering */
|
||||
mByteBuffer.order(mByteOrder);
|
||||
|
||||
while(mByteBuffer.hasRemaining())
|
||||
{
|
||||
convertedSamples[pointer] = mMap.get(mByteBuffer.getShort());
|
||||
pointer++;
|
||||
}
|
||||
|
||||
reusableBuffer.incrementUserCount();
|
||||
return reusableBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set byte interpretation to little or big endian. Defaults to LITTLE
|
||||
* endian
|
||||
*/
|
||||
public void setByteOrder(ByteOrder order)
|
||||
{
|
||||
mByteOrder = order;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +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 io.github.dsheirer.sample.adapter;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Converts 16-bit little endian byte data into an array of floats
|
||||
*/
|
||||
public class ShortAdapter implements ISampleAdapter
|
||||
{
|
||||
private ShortToFloatMap mMap = new ShortToFloatMap();
|
||||
private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN;
|
||||
|
||||
@Override
|
||||
public float[] convert( byte[] samples )
|
||||
{
|
||||
float[] processed = new float[ samples.length / 2 ];
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
/* Wrap byte array in a byte buffer so we can process them as shorts */
|
||||
ByteBuffer buffer = ByteBuffer.wrap( samples );
|
||||
|
||||
/* Set endian to correct byte ordering */
|
||||
buffer.order( mByteOrder );
|
||||
|
||||
while( buffer.hasRemaining() )
|
||||
{
|
||||
processed[ pointer ] = mMap.get( buffer.getShort() );
|
||||
|
||||
pointer++;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set byte interpretation to little or big endian. Defaults to LITTLE
|
||||
* endian
|
||||
*/
|
||||
public void setByteOrder( ByteOrder order )
|
||||
{
|
||||
mByteOrder = order;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.github.dsheirer.sample.adapter;
|
||||
|
||||
import io.github.dsheirer.source.tuner.hackrf.HackRFTunerController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Test
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( HackRFTunerController.class );
|
||||
|
||||
public static void main( String[] args )
|
||||
{
|
||||
mLog.debug( "Starting" );
|
||||
|
||||
ShortToFloatMap map1 = new ShortToFloatMap();
|
||||
|
||||
for( short x = -32768; x < -32758; x++ )
|
||||
{
|
||||
mLog.debug( "X:" + x + " value:" + map1.get( x ) );
|
||||
}
|
||||
mLog.debug( "Finished" );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
283
src/main/java/io/github/dsheirer/source/mixer/MixerReader.java
Normal file
283
src/main/java/io/github/dsheirer/source/mixer/MixerReader.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* 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
|
||||
@@ -88,7 +88,11 @@ public abstract class FCDTunerController extends TunerController
|
||||
}
|
||||
|
||||
mComplexMixer = new ComplexMixer( mixerTDL.getTargetDataLine(), audioFormat, tunerName,
|
||||
new ComplexShortAdapter(), mReusableBufferBroadcaster);
|
||||
new ComplexShortAdapter(mixerTDL.getMixerTunerType().getDisplayString()));
|
||||
|
||||
mComplexMixer.setBufferSize(audioFormat.getFrameSize() * (int)(audioFormat.getSampleRate() * 0.1));
|
||||
|
||||
mComplexMixer.setBufferListener(mReusableBufferBroadcaster);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* 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
|
||||
@@ -45,7 +45,7 @@ public class FCD2TunerController extends FCDTunerController
|
||||
public FCD2TunerController(MixerTunerDataLine mixerTDL, Device device, DeviceDescriptor descriptor)
|
||||
{
|
||||
super(mixerTDL, device, descriptor, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getDisplayString(),
|
||||
MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO.getAudioFormat());
|
||||
MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS.getAudioFormat());
|
||||
}
|
||||
|
||||
public void init() throws SourceException
|
||||
|
||||
Reference in New Issue
Block a user