Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.sample.Broadcaster;
Expand All @@ -44,10 +40,8 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
Expand Down Expand Up @@ -75,12 +69,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
private static final double CHANNEL_OVERSAMPLING = 2.0;
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;

private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
private INativeBufferProvider mNativeBufferProvider;
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
private ChannelCalculator mChannelCalculator;
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
private NativeBufferReceiver mNativeBufferReceiver = new NativeBufferReceiver();
Expand Down Expand Up @@ -181,63 +175,22 @@ public TunerChannelSource getChannel(TunerChannel tunerChannel)

if(mRunning)
{
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);

IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);

if(outputProcessor != null)
try
{
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);

try
{
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
mChannelCalculator.getChannelSampleRate(), centerFrequency);
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
mChannelSourceEventListener);

mChannelSources.add(channelSource);
}
catch(FilterDesignException fde)
{
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
}
mChannelSources.add(channelSource);
}
catch(IllegalArgumentException iae)
{
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
}
}

return channelSource;
}

/**
* Creates a processor to process the channelizer channel indexes into a composite output stream providing
* channelized complex sample buffers to a registered source listener.
* @param indexes to target by the output processor
* @return output processor compatible with the number of indexes to monitor
*/
private IPolyphaseChannelOutputProcessor getOutputProcessor(List<Integer> indexes)
{
switch(indexes.size())
{
case 1:
return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes,
mChannelCalculator.getChannelCount());
case 2:
try
{
float[] filter = getOutputProcessorFilter(2);
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes, filter,
mChannelCalculator.getChannelCount());
}
catch(FilterDesignException fde)
{
mLog.error("Error designing 2 channel synthesis filter for output processor");
}
default:
//TODO: create output processor for greater than 2 input channels
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(mChannelCalculator.toString());
return null;
}
}

/**
* Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive
* sample buffers when this is the first channel.
Expand All @@ -249,7 +202,6 @@ private void startChannelSource(PolyphaseChannelSource channelSource)
synchronized(mBufferDispatcher)
{
//Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method

checkChannelizerConfiguration();

mPolyphaseChannelizer.addChannel(channelSource);
Expand Down Expand Up @@ -311,10 +263,6 @@ public void process(SourceEvent sourceEvent) throws SourceException
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
//Update channel calculator immediately so that channels can be allocated
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());

//Defer channelizer configuration changes to be handled on the buffer processor thread
mNativeBufferReceiver.receive(sourceEvent);
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
Expand Down Expand Up @@ -381,80 +329,22 @@ private void checkChannelizerConfiguration()
* Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which
* would cause the output processors to change the polyphase channelizer results channel(s) that the processor is
* consuming
*
* @param sourceEvent (optional-can be null) to broadcast to each output processor following the update
*/
private void updateOutputProcessors(SourceEvent sourceEvent)
private void updateOutputProcessors()
{
for(PolyphaseChannelSource channelSource: mChannelSources)
{
updateOutputProcessor(channelSource);

//Send the non-null source event to each channel source
if(sourceEvent != null)
{
try
{
channelSource.process(sourceEvent);
}
catch(SourceException se)
{
mLog.error("Error while notifying polyphase channel source of a source event", se);
}
}
}
}

/**
* Updates the polyphase channel source's output processor due to a change in the center frequency or sample
* rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's
* center tuned frequency changes.
*
* @param channelSource that requires an update to its output processor
*/
private void updateOutputProcessor(PolyphaseChannelSource channelSource)
{
try
{
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());

long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);

//If the indexes size is the same then update the current processor, otherwise create a new one
IPolyphaseChannelOutputProcessor outputProcessor = channelSource.getPolyphaseChannelOutputProcessor();

if(outputProcessor != null && outputProcessor.getInputChannelCount() == indexes.size())
try
{
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
channelSource.setFrequency(centerFrequency);

if(indexes.size() > 1)
{
try
{
float[] filter = getOutputProcessorFilter(indexes.size());
channelSource.getPolyphaseChannelOutputProcessor().setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
}
}
channelSource.updateOutputProcessor(mChannelCalculator, mFilterManager);
}
else
catch(IllegalArgumentException iae)
{
channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
mLog.error("Error updating polyphase channel source output processor following tuner frequency or " +
"sample rate change");
stopChannelSource(channelSource);
}
}
catch(IllegalArgumentException iae)
{
mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " +
"updated tuner center frequency and sample rate. Stopping channel source", iae);

stopChannelSource(channelSource);
}
}

/**
Expand Down Expand Up @@ -497,29 +387,6 @@ public void removeSourceEventListener(Listener<SourceEvent> listener)
mSourceEventBroadcaster.removeListener(listener);
}

/**
* Generates (or reuses) an output processor filter for the specified number of channels. Each
* filter is created only once and stored in a map for reuse. This map is cleared anytime that the
* input sample rate changes, so that the filters can be recreated with the new channel sample rate.
* @param channels count
* @return filter
* @throws FilterDesignException if the filter cannot be designed to specification (-6 dB band edge)
*/
private float[] getOutputProcessorFilter(int channels) throws FilterDesignException
{
float[] taps = mOutputProcessorFilters.get(channels);

if(taps == null)
{
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelSampleRate(),
mChannelCalculator.getChannelBandwidth(), channels, POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);

mOutputProcessorFilters.put(channels, taps);
}

return taps;
}

/**
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
*/
Expand Down Expand Up @@ -584,36 +451,32 @@ public void receive(SourceEvent sourceEvent)
*/
public class NativeBufferReceiver implements Listener<INativeBuffer>
{
private Queue<SourceEvent> mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
private boolean mOutputProcessorUpdateRequired = false;

/**
* Queues the source event for deferred execution on the buffer processing thread.
* @param event that affects configuration of the channelizer (frequency or sample rate change events)
*/
public void receive(SourceEvent event)
{
mQueuedSourceEvents.offer(event);
long frequency = event.getValue().longValue();

if(mChannelCalculator.getCenterFrequency() != frequency)
{
mChannelCalculator.setCenterFrequency(frequency);
mOutputProcessorUpdateRequired = true;
}
}

@Override
public void receive(INativeBuffer nativeBuffer)
{
try
{
//Process any queued source events before processing the buffers
SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll();

while(queuedSourceEvent != null)
if(mOutputProcessorUpdateRequired)
{
switch(queuedSourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
//Don't send the tuner's frequency change event down to the channels - it would cause chaos
updateOutputProcessors(null);
break;
}

queuedSourceEvent = mQueuedSourceEvents.poll();
updateOutputProcessors();
mOutputProcessorUpdateRequired = false;
}

if(mPolyphaseChannelizer != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@
package io.github.dsheirer.dsp.filter.channelizer;

import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.StreamProcessorWithHeartbeat;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Polyphase Channelizer's Tuner Channel Source implementation. Wraps a ChannelOutputProcessor instance and
Expand All @@ -36,34 +40,30 @@
*/
public class PolyphaseChannelSource extends TunerChannelSource implements Listener<ComplexSamples>
{
private Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
private IPolyphaseChannelOutputProcessor mReplacementPolyphaseChannelOutputProcessor;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
private long mReplacementFrequency;
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private long mChannelFrequencyCorrection;
private ReentrantLock mOutputProcessorLock = new ReentrantLock();

/**
* Constructs an instance
*
* @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate
* @param outputProcessor - to process polyphase channelizer channel results into a channel stream
* @param channelCalculator for current channel center frequency and sample rate and index calculations
* @param filterManager for access to new or cached synthesis filters
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
* @param channelSampleRate for the downstream sample output
* @param centerFrequency of the incoming polyphase channel(s)
* @throws FilterDesignException if a channel low pass filter can't be designed to the channel specification
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
*/
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate,
long centerFrequency) throws FilterDesignException
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
Listener<SourceEvent> producerSourceEventListener) throws IllegalArgumentException
{
super(producerSourceEventListener, tunerChannel);
mPolyphaseChannelOutputProcessor = outputProcessor;
mPolyphaseChannelOutputProcessor.setListener(this);
mChannelSampleRate = channelSampleRate;
mChannelSampleRate = channelCalculator.getChannelSampleRate();
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
setFrequency(centerFrequency);
updateOutputProcessor(channelCalculator, filterManager);
}

@Override
Expand Down Expand Up @@ -109,58 +109,101 @@ public void receive(ComplexSamples complexSamples)
}

/**
* Channel output processor used by this channel source to convert polyphase channel results into a specific
* channel complex buffer output stream.
* Updates the output processor whenever the source tuner's center frequency changes.
* @param channelCalculator providing access to updated tuner center frequency, sample rate, etc.
* @param filterManager for designing and caching synthesis filters
*/
public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager) throws IllegalArgumentException
{
return mPolyphaseChannelOutputProcessor;
}
String errorMessage = null;

/**
* Sets/updates the output processor for this channel source, replacing the existing output processor.
*
* @param outputProcessor to use
* @param frequency center for the channels processed by the output processor
*/
public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency)
{
//If this is the first time, simply assign the output processor
if(mPolyphaseChannelOutputProcessor == null)
mOutputProcessorLock.lock();

try
{
mPolyphaseChannelOutputProcessor = outputProcessor;
mPolyphaseChannelOutputProcessor.setListener(this);
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());

//The provided channels are necessarily aligned to the center frequency that this source is providing and an
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));

if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
{
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);

if(indexes.size() > 1)
{
try
{
float[] filter = filterManager.getFilter(channelCalculator.getSampleRate(),
channelCalculator.getChannelBandwidth(), indexes.size());
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
}
}

mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
else //Create a new output processor.
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setListener(null);
}

mPolyphaseChannelOutputProcessor = null;

switch(indexes.size())
{
case 1:
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, channelCalculator.getChannelCount());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
break;
case 2:
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), 2);
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, filter, channelCalculator.getChannelCount());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
}
catch(FilterDesignException fde)
{
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
mLog.error("Error creating a synthesis filter for a new channel output processor");
}
break;
default:
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(channelCalculator.toString());
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
indexes.size();
}
}
}
//Otherwise, we have to swap out the processor on the sample processing thread
else
finally
{
mReplacementPolyphaseChannelOutputProcessor = outputProcessor;
mReplacementFrequency = frequency;
mOutputProcessorLock.unlock();
}
}

/**
* Updates the output processor to use the new output processor provided by the external
* polyphase channel manager. This method should only be executed on the sample processing
* thread, within the processChannelResults() method.
*/
private void swapOutputProcessor()
{
if(mReplacementPolyphaseChannelOutputProcessor != null)
//Unlikely, but if we had an error designing a synthesis filter, throw an exception
if(errorMessage != null)
{
IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
existingProcessor.stop();
existingProcessor.setListener(null);

//Swap out the processor so that incoming samples can accumulate in the new channel output processor
mPolyphaseChannelOutputProcessor = mReplacementPolyphaseChannelOutputProcessor;
mReplacementPolyphaseChannelOutputProcessor = null;
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.start();

//Finally, setup the frequency offset for the output processor.
mIndexCenterFrequency = mReplacementFrequency;
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
throw new IllegalArgumentException(errorMessage);
}
}

Expand All @@ -170,15 +213,23 @@ private void swapOutputProcessor()
* channel aggregation, and dispatch the results to the downstream sample listener/consumer.
*
* @param channelResultsList containing a list of polyphase channelizer output arrays.
* @param currentSamplesTimestamp for the samples
*/
public void receiveChannelResults(List<float[]> channelResultsList)
public void receiveChannelResults(List<float[]> channelResultsList, long currentSamplesTimestamp)
{
if(mReplacementPolyphaseChannelOutputProcessor != null)
mOutputProcessorLock.lock();

try
{
swapOutputProcessor();
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList, currentSamplesTimestamp);
}
}
finally
{
mOutputProcessorLock.unlock();
}

mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList);
}

/**
Expand Down Expand Up @@ -226,7 +277,6 @@ public long getChannelFrequencyCorrection()
*/
protected void setChannelFrequencyCorrection(long value)
{
//TODO: push this frequency correction down to the output processor ...
mChannelFrequencyCorrection = value;
updateFrequencyOffset();
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
Expand Down Expand Up @@ -262,7 +312,10 @@ private long getFrequencyOffset()
*/
private void updateFrequencyOffset()
{
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.filter.channelizer;

import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import java.util.HashMap;
import java.util.Map;

/**
* Creates and caches channel output processor synthesis filters.
*/
public class SynthesisFilterManager
{
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
private static final String SEPARATOR = "-";
private Map<String,float[]> mFilterMap = new HashMap<>();

/**
* Design or retrieve a previously cached output processor synthesis filter.
* @param sampleRate of the tuner
* @param channelBandwidth per channel
* @param channelCount as the number of channels being synthesized/aggregated for the output processor (1 or 2)
* @return filter
* @throws FilterDesignException if the filter cannot be designed based on the supplied parameters.
*/
public float[] getFilter(double sampleRate, double channelBandwidth, int channelCount) throws FilterDesignException
{
String key = sampleRate + SEPARATOR + channelBandwidth + SEPARATOR + channelCount;

if(mFilterMap.containsKey(key))
{
return mFilterMap.get(key);
}

float[] taps = FilterFactory.getSincM2Synthesizer(sampleRate, channelBandwidth, channelCount,
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
mFilterMap.put(key, taps);
return taps;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public ComplexSamples process(ComplexSamples channelBuffer1, ComplexSamples chan
mTopBlockFlag = !mTopBlockFlag;
}

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, channelBuffer1.timestamp());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.util.Dispatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor
{
Expand All @@ -34,6 +33,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
private Dispatcher<List<float[]>> mChannelResultsDispatcher;
protected Listener<ComplexSamples> mComplexSamplesListener;
private int mInputChannelCount;
private long mCurrentSampleTimestamp = System.currentTimeMillis();

/**
* Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
Expand All @@ -50,6 +50,15 @@ public ChannelOutputProcessor(int inputChannelCount, double sampleRate)
mChannelResultsDispatcher.setListener(floats -> process(floats));
}

/**
* Timestamp for the current series of samples.
* @return time in milliseconds to use with assembled complex sample buffers.
*/
protected long getCurrentSampleTimestamp()
{
return mCurrentSampleTimestamp;
}

@Override
public void start()
{
Expand Down Expand Up @@ -82,9 +91,10 @@ public void dispose()
}

@Override
public void receiveChannelResults(List<float[]> channelResultsList)
public void receiveChannelResults(List<float[]> channelResultsList, long timestamp)
{
mChannelResultsDispatcher.receive(channelResultsList);
mCurrentSampleTimestamp = timestamp;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;

import java.util.List;

public interface IPolyphaseChannelOutputProcessor
Expand All @@ -37,19 +36,15 @@ public interface IPolyphaseChannelOutputProcessor
/**
* Receive and enqueue output results from the polyphase analysis channelizer
* @param channelResults to enqueue
* @param timestamp for the first channel results buffer
*/
void receiveChannelResults(List<float[]> channelResults);
void receiveChannelResults(List<float[]> channelResults, long timestamp);

/**
* Listener to receive assembled complex samples buffers
*/
void setListener(Listener<ComplexSamples> listener);

// /**
// * Process the channel output channel results queue and deliver the output to the listener
// */
// void processChannelResults();

/**
* Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset
* frequency to produce an output where the desired signal is centered in the passband.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ protected ComplexGain getGain()

/**
* Access the fully assembled buffer when hasBuffer() indicates that a buffer is ready.
* @param timestamp to use for the assembled complex samples buffer
* @return true if a full buffer is assembled and ready.
*/
public abstract ComplexSamples getBuffer();
public abstract ComplexSamples getBuffer(long timestamp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ public boolean hasBuffer()
* @return assembled buffer.
*/
@Override
public ComplexSamples getBuffer()
public ComplexSamples getBuffer(long timestamp)
{
ComplexSamples buffer = mSampleAssembler.getBufferAndReset();
ComplexSamples buffer = mSampleAssembler.getBufferAndReset(timestamp);

if(getMixer().hasFrequency())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@
package io.github.dsheirer.dsp.filter.channelizer.output;

import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class OneChannelOutputProcessor extends ChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class);
Expand Down Expand Up @@ -80,7 +79,7 @@ public void setFrequencyOffset(long frequency)
* Extract the channel from the channel results array and pass to the assembler. The assembler will
* apply frequency translation and gain and indicate when a buffer is fully assembled.
*
* @param lists to process containing a list of a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param channelResultsList to process containing a list of a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
*/
@Override
public void process(List<float[]> channelResultsList)
Expand All @@ -91,7 +90,7 @@ public void process(List<float[]> channelResultsList)

if(mMixerAssembler.hasBuffer())
{
ComplexSamples buffer = mMixerAssembler.getBuffer();
ComplexSamples buffer = mMixerAssembler.getBuffer(getCurrentSampleTimestamp());

if(mComplexSamplesListener != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ public boolean hasBuffer()
/**
* Extracts the full buffer from this assembler and resets the assembler to accept more samples.
*/
public ComplexSamples getBufferAndReset()
public ComplexSamples getBufferAndReset(long timestamp)
{
ComplexSamples buffer = new ComplexSamples(mI, mQ);
ComplexSamples buffer = new ComplexSamples(mI, mQ, timestamp);
reset();
return buffer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,14 @@ public boolean hasBuffer()

/**
* Access the assembled buffer. Frequency offset and gain are applied to the returned buffer.
* @param timestamp to use for the assembled buffer
* @return assembled buffer.
*/
@Override
public ComplexSamples getBuffer()
public ComplexSamples getBuffer(long timestamp)
{
ComplexSamples channel1 = mChannel1SampleAssembler.getBufferAndReset();
ComplexSamples channel2 = mChannel2SampleAssembler.getBufferAndReset();
ComplexSamples channel1 = mChannel1SampleAssembler.getBufferAndReset(timestamp);
ComplexSamples channel2 = mChannel2SampleAssembler.getBufferAndReset(timestamp);

//Synthesize two channels into one, reducing sample rate by 2 so it's same as single channel rate
ComplexSamples buffer = mTwoChannelSynthesizer.process(channel1, channel2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@
package io.github.dsheirer.dsp.filter.channelizer.output;

import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class TwoChannelOutputProcessor extends ChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
Expand Down Expand Up @@ -91,7 +90,7 @@ public void setPolyphaseChannelIndices(List<Integer> indexes)
* Extract the channel from the channel results array, apply frequency translation, and deliver the
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
*
* @param lists to process containing a list of a list of an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param channelResultsList to process containing a list of a list of an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
*/
@Override
public void process(List<float[]> channelResultsList)
Expand All @@ -103,7 +102,7 @@ public void process(List<float[]> channelResultsList)

if(mMixerAssembler.hasBuffer())
{
ComplexSamples buffer = mMixerAssembler.getBuffer();
ComplexSamples buffer = mMixerAssembler.getBuffer(getCurrentSampleTimestamp());

if(mComplexSamplesListener != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ public HilbertTransform()
/**
* Converts the real sample array to complex samples as half the sample rate
* @param samples to convert
* @param timestamp of the first sample
* @return converted samples
*/
public abstract ComplexSamples filter(float[] samples);
public abstract ComplexSamples filter(float[] samples, long timestamp);

/**
* Converts the half-band filter coefficients for use as hilbert transform filter coefficients. Sets all
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import io.github.dsheirer.dsp.oscillator.OscillatorFactory;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;

import java.util.Arrays;

/**
Expand All @@ -35,9 +34,10 @@ public class ScalarHilbertTransform extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;

Expand All @@ -52,7 +52,7 @@ public ComplexSamples filter(float[] realSamples)
mQBuffer = qTemp;
}

ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
System.arraycopy(deinterleaved.q(), 0, mQBuffer, mQOverlap, deinterleaved.q().length);

Expand All @@ -78,7 +78,7 @@ public ComplexSamples filter(float[] realSamples)
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}

public static void main(String[] args)
Expand All @@ -104,8 +104,8 @@ public static void main(String[] args)

if(validate)
{
ComplexSamples scalar = hilbertTransform.filter(realSamples);
ComplexSamples vector = vector512.filter(realSamples);
ComplexSamples scalar = hilbertTransform.filter(realSamples, 0l);
ComplexSamples vector = vector512.filter(realSamples, start);

System.out.println("SI: " + Arrays.toString(scalar.i()));
System.out.println("VI: " + Arrays.toString(vector.i()));
Expand All @@ -119,7 +119,7 @@ public static void main(String[] args)
{
// ComplexSamples filtered = hilbertTransform.filter(realSamples);
// ComplexSamples filtered = vectorDefault.filter(realSamples);
ComplexSamples filtered = vector512.filter(realSamples);
ComplexSamples filtered = vector512.filter(realSamples, start);
// ComplexSamples filtered = vector256.filter(realSamples);
// ComplexSamples filtered = vector128.filter(realSamples);
// ComplexSamples filtered = vector64.filter(realSamples);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;

Expand All @@ -54,7 +55,7 @@ public ComplexSamples filter(float[] realSamples)
mQBuffer = qTemp;
}

ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);

System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
Expand Down Expand Up @@ -91,6 +92,6 @@ public ComplexSamples filter(float[] realSamples)
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;

Expand All @@ -54,7 +55,7 @@ public ComplexSamples filter(float[] realSamples)
mQBuffer = qTemp;
}

ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);

System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
Expand Down Expand Up @@ -85,6 +86,6 @@ public ComplexSamples filter(float[] realSamples)
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.vector.VectorUtilities;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

/**
* Implements a Hilbert transform using vector SIMD operations against a pre-defined 47-tap filter.
* As described in Understanding Digital Signal Processing, Lyons, 3e, 2011, Section 13.37.2 (p 804-805)
Expand All @@ -53,9 +52,10 @@ public VectorHilbertTransform512Bits()
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;

Expand All @@ -70,7 +70,7 @@ public ComplexSamples filter(float[] realSamples)
mQBuffer = qTemp;
}

ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);

System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
Expand Down Expand Up @@ -103,6 +103,6 @@ public ComplexSamples filter(float[] realSamples)
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;

Expand All @@ -54,7 +55,7 @@ public ComplexSamples filter(float[] realSamples)
mQBuffer = qTemp;
}

ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);

System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
Expand Down Expand Up @@ -103,6 +104,6 @@ public ComplexSamples filter(float[] realSamples)
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;

Expand All @@ -54,7 +55,7 @@ public ComplexSamples filter(float[] realSamples)
mQBuffer = qTemp;
}

ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);

System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
Expand Down Expand Up @@ -84,6 +85,6 @@ public ComplexSamples filter(float[] realSamples)
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
24 changes: 22 additions & 2 deletions src/main/java/io/github/dsheirer/dsp/gain/complex/ComplexGain.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.gain.complex;

import io.github.dsheirer.sample.complex.ComplexSamples;
Expand Down Expand Up @@ -46,7 +65,7 @@ protected void setGain(float gain)
*/
public ComplexSamples apply(ComplexSamples samples)
{
return apply(samples.i(), samples.q());
return apply(samples.i(), samples.q(), samples.timestamp());
}

/**
Expand All @@ -56,7 +75,8 @@ public ComplexSamples apply(ComplexSamples samples)
* original sample buffer.
* @param i samples to amplify
* @param q samples to amplify
* @param timestamp of the first sample
* @return amplified samples
*/
public abstract ComplexSamples apply(float[] i, float q[]);
public abstract ComplexSamples apply(float[] i, float q[], long timestamp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,9 @@
*/
package io.github.dsheirer.dsp.gain.complex;

import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;

import java.text.DecimalFormat;
import java.util.Arrays;

public class ComplexGainControl implements IComplexGainControl
{
public static final float OBJECTIVE_ENVELOPE = 1.0f;
Expand All @@ -48,9 +44,10 @@ public ComplexGainControl()
* gain for the single sample in the buffer that has the largest envelope.
* @param i samples
* @param q samples
* @param timestamp of the first sample
* @return processed/amplified buffer
*/
@Override public ComplexSamples process(float[] i, float[] q)
@Override public ComplexSamples process(float[] i, float[] q, long timestamp)
{
float maxEnvelope = MINIMUM_ENVELOPE;

Expand All @@ -70,45 +67,6 @@ public ComplexGainControl()
qProcessed[x] = q[x] * gain;
}

return new ComplexSamples(iProcessed, qProcessed);
}

public static void main(String[] args)
{
int sampleSize = 2048;
ComplexSamples samples = SampleUtils.generateComplexRandomVectorLength(sampleSize);
ComplexGainControl scalar = new ComplexGainControl();
VectorComplexGainControl vector = new VectorComplexGainControl();

boolean validate = false;

if(validate)
{
ComplexSamples scalarSamples = scalar.process(samples.i(), samples.q());
ComplexSamples vectorSamples = vector.process(samples.i(), samples.q());

System.out.println("SCALAR I: " + Arrays.toString(scalarSamples.i()));
System.out.println("VECTOR I: " + Arrays.toString(vectorSamples.i()));
System.out.println("SCALAR Q: " + Arrays.toString(scalarSamples.q()));
System.out.println("VECTOR Q: " + Arrays.toString(vectorSamples.q()));
}
else
{
double accumulator = 0.0d;
int iterations = 10_000_000;
long start = System.currentTimeMillis();

for(int x = 0; x < iterations; x++)
{
// ComplexSamples processed = scalar.process(samples.i(), samples.q());
ComplexSamples processed = vector.process(samples.i(), samples.q());
accumulator += processed.i()[3];
}

double elapsed = System.currentTimeMillis() - start;
DecimalFormat df = new DecimalFormat("0.000");
System.out.println("Accumulator: " + accumulator);
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
}
return new ComplexSamples(iProcessed, qProcessed, timestamp);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.gain.complex;

import io.github.dsheirer.sample.complex.ComplexSamples;
Expand All @@ -13,5 +32,5 @@ public interface IComplexGainControl
* @param q samples
* @return amplified gain samples
*/
ComplexSamples process(float[] i, float[] q);
ComplexSamples process(float[] i, float[] q, long timestamp);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.gain.complex;

import io.github.dsheirer.sample.complex.ComplexSamples;
Expand All @@ -22,14 +41,14 @@ public ScalarComplexGain(float gain)
* @param q samples to amplify
* @return amplified samples
*/
@Override public ComplexSamples apply(float[] i, float[] q)
@Override public ComplexSamples apply(float[] i, float[] q, long timestamp)
{
for(int x = 0; x < i.length; x++)
{
i[x] *= mGain;
q[x] *= mGain;
}

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.gain.complex;

import io.github.dsheirer.sample.complex.ComplexSamples;
Expand Down Expand Up @@ -27,7 +46,7 @@ public VectorComplexGain(float gain)
* @param q samples to amplify
* @return amplified samples.
*/
@Override public ComplexSamples apply(float[] i, float[] q)
@Override public ComplexSamples apply(float[] i, float[] q, long timestamp)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);

Expand All @@ -37,6 +56,6 @@ public VectorComplexGain(float gain)
FloatVector.fromArray(VECTOR_SPECIES, q, x).mul(mGain).intoArray(q, x);
}

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.gain.complex;

import io.github.dsheirer.sample.complex.ComplexSamples;
Expand All @@ -18,7 +37,7 @@ public class VectorComplexGainControl implements IComplexGainControl
private static final float MINIMUM_ENVELOPE = 0.0001f;
private static final float ENVELOPE_ESTIMATE = 0.4f;

@Override public ComplexSamples process(float[] i, float[] q)
@Override public ComplexSamples process(float[] i, float[] q, long timestamp)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);

Expand Down Expand Up @@ -47,6 +66,6 @@ public class VectorComplexGainControl implements IComplexGainControl
FloatVector.fromArray(VECTOR_SPECIES, q, bufferPointer).mul(gain).intoArray(qProcessed, bufferPointer);
}

return new ComplexSamples(iProcessed, qProcessed);
return new ComplexSamples(iProcessed, qProcessed, timestamp);
}
}
11 changes: 5 additions & 6 deletions src/main/java/io/github/dsheirer/dsp/mixer/ComplexMixer.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ public void setSampleRate(double sampleRate)
/**
* Generates complex samples from the underlying oscillator
* @param sampleCount to generate
* @param timestamp of the first sample
* @return complex samples
*/
protected ComplexSamples generate(int sampleCount)
protected ComplexSamples generate(int sampleCount, long timestamp)
{
return mOscillator.generateComplexSamples(sampleCount);
return mOscillator.generateComplexSamples(sampleCount, timestamp);
}

/**
Expand All @@ -98,7 +99,7 @@ protected ComplexSamples generate(int sampleCount)
*/
public ComplexSamples mix(ComplexSamples samples)
{
return mix(samples.i(), samples.q());
return mix(samples.i(), samples.q(), samples.timestamp());
}

/**
Expand All @@ -117,7 +118,5 @@ public ComplexSamples mix(InterleavedComplexSamples samples)
* @param q complex samples to mix
* @return mixed samples
*/
public abstract ComplexSamples mix(float[] i, float[] q);


public abstract ComplexSamples mix(float[] i, float[] q, long timestamp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ public ScalarComplexMixer(double frequency, double sampleRate)
* Mixes the complex I & Q samples with samples generated from an oscillator.
* @param iSamples complex samples to mix
* @param qSamples complex samples to mix
* @param timestamp of the first sample
* @return mixed samples
*/
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples)
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp)
{
ComplexSamples mixer = generate(iSamples.length);
ComplexSamples mixer = generate(iSamples.length, timestamp);

float[] iMixer = mixer.i();
float[] qMixer = mixer.q();
Expand All @@ -82,7 +83,7 @@ public ScalarComplexMixer(double frequency, double sampleRate)
@Override
public ComplexSamples mix(InterleavedComplexSamples samples)
{
ComplexSamples mixer = generate(samples.samples().length / 2);
ComplexSamples mixer = generate(samples.samples().length / 2, samples.timestamp());

float[] iMixer = mixer.i();
float[] qMixer = mixer.q();
Expand Down
24 changes: 22 additions & 2 deletions src/main/java/io/github/dsheirer/dsp/mixer/VectorComplexMixer.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.dsp.mixer;

import io.github.dsheirer.sample.complex.ComplexSamples;
Expand Down Expand Up @@ -26,14 +45,15 @@ public VectorComplexMixer(double frequency, double sampleRate)
* Mixes the complex I & Q samples with samples generated from an oscillator.
* @param iSamples complex samples to mix
* @param qSamples complex samples to mix
* @param timestamp for the first sample
* @return mixed samples
*/
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples)
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp)
{
VectorUtilities.checkComplexArrayLength(iSamples, qSamples, VECTOR_SPECIES);

//Reuse this complex samples buffer to store the results and return to caller
ComplexSamples mixer = generate(iSamples.length);
ComplexSamples mixer = generate(iSamples.length, timestamp);

float[] iMixer = mixer.i();
float[] qMixer = mixer.q();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public interface IComplexOscillator extends IOscillator
/**
* Generates the specified number of complex samples into a complex samples object.
* @param sampleCount number of complex samples to generate.
* @param timestamp for the first sample
* @return generated samples
*/
ComplexSamples generateComplexSamples(int sampleCount);
ComplexSamples generateComplexSamples(int sampleCount, long timestamp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public float[] generate(int sampleCount)
}

@Override
public ComplexSamples generateComplexSamples(int sampleCount)
public ComplexSamples generateComplexSamples(int sampleCount, long timestamp)
{
float[] iSamples = new float[sampleCount];
float[] qSamples = new float[sampleCount];
Expand Down Expand Up @@ -133,6 +133,6 @@ public ComplexSamples generateComplexSamples(int sampleCount)
mPreviousInphase = previousInphase;
mPreviousQuadrature = previousQuadrature;

return new ComplexSamples(iSamples, qSamples);
return new ComplexSamples(iSamples, qSamples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@

import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.vector.VectorUtilities;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;

/**
* Complex oscillator that uses JDK17 SIMD vector operations to generate complex sample arrays.
*
Expand Down Expand Up @@ -145,9 +144,10 @@ protected void update()
/**
* Generates complex samples.
* @param sampleCount number of samples to generate and length of the resulting float array.
* @param timestamp of the first sample
* @return generated samples
*/
@Override public ComplexSamples generateComplexSamples(int sampleCount)
@Override public ComplexSamples generateComplexSamples(int sampleCount, long timestamp)
{
if(sampleCount % VECTOR_SPECIES.length() != 0)
{
Expand Down Expand Up @@ -184,6 +184,6 @@ protected void update()
previousInphase.intoArray(mPreviousInphases, 0);
previousQuadrature.intoArray(mPreviousQuadratures, 0);

return new ComplexSamples(iSamples, qSamples);
return new ComplexSamples(iSamples, qSamples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
import io.github.dsheirer.bits.IBinarySymbolProcessor;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;

/**
* Assembles reusable byte buffers from an incoming stream of boolean values.
*/
Expand Down Expand Up @@ -58,6 +57,7 @@ private void getNextBuffer()
{
if(mCurrentBuffer != null && mBufferListener != null)
{
mCurrentBuffer.flip();
mBufferListener.receive(mCurrentBuffer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@

import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;

/**
* Assembles reusable byte buffers from an incoming stream of Dibits.
*/
Expand Down Expand Up @@ -57,6 +56,7 @@ private void getNextBuffer()
{
if(mCurrentBuffer != null && mBufferListener != null)
{
mCurrentBuffer.flip();
mBufferListener.receive(mCurrentBuffer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
import io.github.dsheirer.spectrum.SpectrumPanel;
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
import io.github.dsheirer.util.ThreadPool;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.util.concurrent.TimeUnit;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -45,9 +48,6 @@
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.util.concurrent.TimeUnit;

public class SynthesizerViewer extends JFrame
{
Expand Down Expand Up @@ -281,15 +281,15 @@ public DataGenerationManager()
@Override
public void run()
{
ComplexSamples channel1Buffer = getChannel1ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle);
ComplexSamples channel2Buffer = getChannel2ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle);
ComplexSamples channel1Buffer = getChannel1ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle, 0l);
ComplexSamples channel2Buffer = getChannel2ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle, 0l);

ComplexSamples synthesizedBuffer = mSynthesizer.process(channel1Buffer, channel2Buffer);
getChannel1Panel().receive(new FloatNativeBuffer(channel1Buffer, System.currentTimeMillis()));
getChannel1Panel().receive(new FloatNativeBuffer(channel1Buffer));

getChannel2Panel().receive(new FloatNativeBuffer(channel2Buffer, System.currentTimeMillis()));
getChannel2Panel().receive(new FloatNativeBuffer(channel2Buffer));

getSpectrumPanel().receive(new FloatNativeBuffer(synthesizedBuffer, System.currentTimeMillis()));
getSpectrumPanel().receive(new FloatNativeBuffer(synthesizedBuffer));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
import io.github.dsheirer.rrapi.type.Voice;
import io.github.dsheirer.service.radioreference.RadioReference;
import io.github.dsheirer.util.ThreadPool;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
Expand All @@ -55,12 +60,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
* Radio Reference editor for trunked radio systems
*/
Expand Down Expand Up @@ -293,7 +292,13 @@ private void setSystem(System system)
List<EnrichedSite> enrichedSites = new ArrayList<>();
for(Site site: sites)
{
CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
CountyInfo countyInfo = null;

if(site.getCountyId() > 0)
{
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
}

enrichedSites.add(new EnrichedSite(site, countyInfo));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.github.dsheirer.preference.mp3.MP3Preference;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
Expand Down Expand Up @@ -99,12 +100,47 @@ private ComboBox<MP3Setting> getMP3SettingComboBox()
mMP3SettingComboBox.getItems().addAll(MP3Setting.values());
mMP3SettingComboBox.getSelectionModel().select(mMP3Preference.getMP3Setting());
mMP3SettingComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mMP3Preference.setMP3Setting(newValue));
.addListener((observable, oldValue, newValue) -> {
mMP3Preference.setMP3Setting(newValue);
updateAudioSampleRateComboBox();
});
}

return mMP3SettingComboBox;
}

/**
* Updates the input sample rate combo box values and alerts the user whenever we auto-change an input sample rate
* due to a change in the MP3 setting value.
*/
private void updateAudioSampleRateComboBox()
{
InputAudioFormat currentSelection = getAudioSampleRateComboBox().getSelectionModel().getSelectedItem();
MP3Setting setting = getMP3SettingComboBox().getSelectionModel().getSelectedItem();

getAudioSampleRateComboBox().getItems().clear();
getAudioSampleRateComboBox().getItems().addAll(setting.getSupportedSampleRates());

if(currentSelection != null && getAudioSampleRateComboBox().getItems().contains(currentSelection))
{
getAudioSampleRateComboBox().getSelectionModel().select(currentSelection);
}
else
{
getAudioSampleRateComboBox().getSelectionModel().select(InputAudioFormat.getDefault());

Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Sample Rate Updated");
alert.setHeaderText("Input Sample Rate Updated");

Label wrappingLabel = new Label("Previous input sample rate [" + currentSelection +
"] is not supported with encoder setting [" + setting + "]. Sample rate updated to default.");
wrappingLabel.setWrapText(true);
alert.getDialogPane().setContent(wrappingLabel);
alert.show();
}
}

private ComboBox<InputAudioFormat> getAudioSampleRateComboBox()
{
if(mAudioSampleRateComboBox == null)
Expand All @@ -113,7 +149,12 @@ private ComboBox<InputAudioFormat> getAudioSampleRateComboBox()
mAudioSampleRateComboBox.getItems().addAll(InputAudioFormat.values());
mAudioSampleRateComboBox.getSelectionModel().select(mMP3Preference.getAudioSampleRate());
mAudioSampleRateComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mMP3Preference.setAudioSampleRate(newValue));
.addListener((observable, oldValue, newValue) -> {
if(newValue != null)
{
mMP3Preference.setAudioSampleRate(newValue);
}
});
}

return mAudioSampleRateComboBox;
Expand Down
43 changes: 22 additions & 21 deletions src/main/java/io/github/dsheirer/icon/IconModel.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/*
* *********************************************************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2022 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 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.
* 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/>
* *********************************************************************************************************************
* 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.icon;

Expand All @@ -22,15 +24,6 @@
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.ImageIcon;
import java.awt.Image;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -43,6 +36,15 @@
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.ImageIcon;

public class IconModel
{
Expand Down Expand Up @@ -241,7 +243,6 @@ public static ImageIcon getScaledIcon(String path, int height)
if(path != null)
{
Icon icon = new Icon("", path);

return getScaledIcon(icon.getIcon(), height);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.identifier.tone;

import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.protocol.Protocol;
import java.util.Collections;
import java.util.List;

/**
* IMessage wrapper for a ToneIdentifier decoded from a sequence of AMBE audio frames.
* @param protocol of the decoded tone
* @param timeslot where the tone occurred
* @param timestamp of the event
* @param toneIdentifier that was decoded
* @param messageText to convey with the tone identifier
*/
public record ToneIdentifierMessage(Protocol protocol, int timeslot, long timestamp, ToneIdentifier toneIdentifier,
String messageText) implements IMessage
{
@Override
public long getTimestamp()
{
return timestamp();
}

@Override
public boolean isValid()
{
return true;
}

@Override
public Protocol getProtocol()
{
return protocol();
}

@Override
public int getTimeslot()
{
return timeslot();
}

@Override
public List<Identifier> getIdentifiers()
{
return Collections.singletonList(toneIdentifier());
}

@Override
public String toString()
{
return messageText();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,11 @@
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* DMR decoder module.
Expand Down Expand Up @@ -147,15 +146,15 @@ public void setSampleRate(double sampleRate)
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());

float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());

//Process buffer for power measurements
mPowerMonitor.process(i, q);

ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,20 @@ public DMRDecoderInstrumented(DecodeConfigDMR config)
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());

float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());

if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
}

//Process buffer for power measurements
mPowerMonitor.process(i, q);

ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@
import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.IdentifierUpdateProvider;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.tone.AmbeTone;
import io.github.dsheirer.identifier.tone.Tone;
import io.github.dsheirer.identifier.tone.ToneIdentifier;
import io.github.dsheirer.identifier.tone.ToneIdentifierMessage;
import io.github.dsheirer.identifier.tone.ToneSequence;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageProvider;
import io.github.dsheirer.module.decode.dmr.identifier.DMRToneIdentifier;
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
Expand All @@ -40,19 +42,19 @@
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.sample.Listener;
import jmbe.iface.IAudioWithMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import jmbe.iface.IAudioWithMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* DMR Audio Module for converting transmitted AMBE audio frames to 8 kHz PCM audio
*/
public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider
public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider, IMessageProvider
{
private final static Logger mLog = LoggerFactory.getLogger(DMRAudioModule.class);
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
Expand All @@ -61,6 +63,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
private List<byte[]> mQueuedAmbeFrames = new ArrayList<>();
private boolean mEncryptedCallStateEstablished = false;
private boolean mEncryptedCall = false;
private Listener<IMessage> mMessageListener;

/**
* Constructs an instance
Expand Down Expand Up @@ -111,7 +114,7 @@ public void receive(IMessage message)
List<byte[]> frames = vm.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame);
processAudio(frame, message.getTimestamp());
}
}
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
Expand All @@ -127,15 +130,15 @@ else if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage vo
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame);
processAudio(frame, message.getTimestamp());
}
}
else if(message instanceof VoiceMessage voiceMessage)
{
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame);
processAudio(frame, message.getTimestamp());
}
}
else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader)
Expand Down Expand Up @@ -171,7 +174,7 @@ else if(message instanceof Terminator)
* Processes the audio frame. Queues the frame until encryption state is determined. Once determined, the audio
* frames are dequeued and audio is generated.
*/
private void processAudio(byte[] frame)
private void processAudio(byte[] frame, long timestamp)
{
if(mEncryptedCallStateEstablished)
{
Expand All @@ -182,7 +185,7 @@ private void processAudio(byte[] frame)
{
for(byte[] queuedFrame: mQueuedAmbeFrames)
{
produceAudio(queuedFrame);
produceAudio(queuedFrame, timestamp);
}

mQueuedAmbeFrames.clear();
Expand All @@ -191,21 +194,21 @@ private void processAudio(byte[] frame)
mQueuedAmbeFrames.clear();
}

produceAudio(frame);
produceAudio(frame, timestamp);
}
else
{
mQueuedAmbeFrames.add(frame);
}
}

private void produceAudio(byte[] frame)
private void produceAudio(byte[] frame, long timestamp)
{
try
{
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(frame);
addAudio(audioWithMetadata.getAudio());
processMetadata(audioWithMetadata);
processMetadata(audioWithMetadata, timestamp);
}
catch(Exception e)
{
Expand All @@ -217,19 +220,19 @@ private void produceAudio(byte[] frame)
* Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the
* tone metadata can be converted into a FROM identifier and included with any call segment.
*/
private void processMetadata(IAudioWithMetadata audioWithMetadata)
private void processMetadata(IAudioWithMetadata audioWithMetadata, long timestamp)
{
if(audioWithMetadata.hasMetadata())
{
//JMBE only places 1 entry in the map, but for consistency we'll process the map entry set
for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet())
{
//Each metadata map entry contains a tone-type (key) and tone (value)
Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());
ToneIdentifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());

if(metadataIdentifier != null)
{
broadcast(metadataIdentifier);
broadcast(metadataIdentifier, timestamp);
}
}
}
Expand All @@ -242,13 +245,25 @@ private void processMetadata(IAudioWithMetadata audioWithMetadata)
/**
* Broadcasts the identifier to a registered listener
*/
private void broadcast(Identifier identifier)
private void broadcast(ToneIdentifier identifier, long timestamp)
{
if(mIdentifierUpdateNotificationListener != null)
{
mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier,
IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
}

if(mMessageListener != null)
{
StringBuilder sb = new StringBuilder();
sb.append("DMR Timeslot ");
sb.append(getTimeslot());
sb.append("Audio Tone Sequence Decoded: ");
sb.append(identifier.toString());

mMessageListener.receive(new ToneIdentifierMessage(Protocol.DMR, getTimeslot(), timestamp,
identifier, sb.toString()));
}
}

/**
Expand All @@ -269,6 +284,25 @@ public void removeIdentifierUpdateListener()
mIdentifierUpdateNotificationListener = null;
}

/**
* Registers the listener to receive tone identifier messages from this module.
* @param listener to receive messages
*/
@Override
public void setMessageListener(Listener<IMessage> listener)
{
mMessageListener = listener;
}

/**
* Unregisters the message listener
*/
@Override
public void removeMessageListener()
{
mMessageListener = null;
}

/**
* Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
* flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
Expand Down Expand Up @@ -310,7 +344,7 @@ public void reset()
* @param value of tone
* @return an identifier with the accumulated tone metadata set
*/
public Identifier process(String type, String value)
public ToneIdentifier process(String type, String value)
{
if(type == null || value == null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.module.decode.event;

Expand All @@ -30,13 +27,13 @@
import io.github.dsheirer.module.decode.event.filter.EventFilterProvider;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class DecodeEventModel extends AbstractTableModel implements Listener<IDecodeEvent>, EventFilterProvider<IDecodeEvent>
{
Expand Down Expand Up @@ -149,10 +146,12 @@ public void setMaxMessageCount(int count)
*/
public void receive(final IDecodeEvent event)
{
if (!mEventFilterSet.passes(event))
{
return;
}
//Disabled until #1368 decode event is fully implemented
// if (!mEventFilterSet.passes(event))
// {
// return;
// }

if(!mEvents.contains(event))
{
mEvents.add(0, event);
Expand Down
Loading