mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Closes #7 NXDN Decoder * Implements: NXDN4800, NXDN9600, Type-C and Type-D decoding and trunk tracking. * Redesigns tuner and channel frequency error management.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -607,6 +607,22 @@ public class FilterFactory
|
||||
return cicArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Root Raised Cosine filter.
|
||||
* @param samplesPerSymbol
|
||||
* @param symbolCount
|
||||
* @param alpha
|
||||
* @return
|
||||
*/
|
||||
public static float[] getRRC(double samplesPerSymbol, int symbolCount, float alpha)
|
||||
{
|
||||
//TODO: this wraps the existing method which is reducing the samples per symbol value x2 internally and is likely
|
||||
// a mistake. This method is used by NXDN to correct that error until the DMR and P25 decoders can be updated to
|
||||
// ensure that the correction doesn't impact those decoders. Once the cleanup is complete, this method can be
|
||||
// deleted and the NXDN implementation can revert to using the future corrected version.
|
||||
return getRootRaisedCosine(samplesPerSymbol * 2, symbolCount, alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1.
|
||||
*
|
||||
@@ -963,11 +979,11 @@ public class FilterFactory
|
||||
* @return filter coefficients.
|
||||
* @throws FilterDesignException if the requested length is not odd
|
||||
*/
|
||||
public static float[] getSinc(double cutoff, int length, WindowType windowType) throws FilterDesignException
|
||||
public static float[] getSinc(double cutoff, int length, WindowType windowType)
|
||||
{
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
throw new FilterDesignException("Sinc filters must be odd-length");
|
||||
throw new IllegalArgumentException("Sinc filters must be odd-length");
|
||||
}
|
||||
|
||||
float[] coefficients = new float[length];
|
||||
@@ -983,7 +999,46 @@ public class FilterFactory
|
||||
for(int x = 1; x <= half; x++)
|
||||
{
|
||||
double a = piScalor * x;
|
||||
double coefficient = scalor * FastMath.sin(a) / a;
|
||||
double coefficient = scalor * (FastMath.sin(a) / a);
|
||||
|
||||
coefficient *= window[half + x];
|
||||
coefficients[half + x] = (float)coefficient;
|
||||
coefficients[half - x] = (float)coefficient;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a windowed-sync (Nyquist) filter.
|
||||
*
|
||||
* @param cutoff frequency (0.0 <> 0.5)
|
||||
* @param length of the filter - must be odd-length
|
||||
* @param windowType to use in designing the filter
|
||||
* @return filter coefficients.
|
||||
* @throws FilterDesignException if the requested length is not odd
|
||||
*/
|
||||
public static float[] getInverseSync(double cutoff, int length, WindowType windowType)
|
||||
{
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Sinc filters must be odd-length");
|
||||
}
|
||||
|
||||
float[] coefficients = new float[length];
|
||||
int half = length / 2;
|
||||
|
||||
float[] window = WindowFactory.getWindow(windowType, length);
|
||||
|
||||
double scalor = 2.0 * cutoff;
|
||||
double piScalor = FastMath.PI * scalor;
|
||||
|
||||
coefficients[half] = (float)(1.0 * scalor * window[half]);
|
||||
|
||||
for(int x = 1; x <= half; x++)
|
||||
{
|
||||
double a = piScalor * x;
|
||||
double coefficient = scalor * (a / FastMath.sin(a));
|
||||
|
||||
coefficient *= window[half + x];
|
||||
coefficients[half + x] = (float)coefficient;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -19,7 +19,6 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.buffer.INativeBuffer;
|
||||
import io.github.dsheirer.buffer.INativeBufferProvider;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
@@ -73,7 +72,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
|
||||
|
||||
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||
private INativeBufferProvider mNativeBufferProvider;
|
||||
private TunerController mTunerController;
|
||||
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||
private ChannelCalculator mChannelCalculator;
|
||||
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
|
||||
@@ -85,23 +84,20 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
private boolean mRunning = true;
|
||||
|
||||
/**
|
||||
* Creates a polyphase channel manager instance.
|
||||
* Creates a polyphase channel manager for the tuner controller
|
||||
*
|
||||
* @param nativeBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer
|
||||
* streams
|
||||
* @param frequency of the baseband complex buffer sample stream (ie center frequency)
|
||||
* @param sampleRate of the baseband complex buffer sample stream
|
||||
* @param tunerController for a tuner that provides a baseband complex buffer stream.
|
||||
*/
|
||||
public PolyphaseChannelManager(INativeBufferProvider nativeBufferProvider, long frequency, double sampleRate)
|
||||
public PolyphaseChannelManager(TunerController tunerController)
|
||||
{
|
||||
if(nativeBufferProvider == null)
|
||||
if(tunerController == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
|
||||
}
|
||||
|
||||
mNativeBufferProvider = nativeBufferProvider;
|
||||
mTunerController = tunerController;
|
||||
|
||||
int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
|
||||
int channelCount = (int)(tunerController.getSampleRate() / MINIMUM_CHANNEL_BANDWIDTH);
|
||||
|
||||
//Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer
|
||||
if(channelCount % 2 != 0)
|
||||
@@ -109,21 +105,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
channelCount--;
|
||||
}
|
||||
|
||||
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
|
||||
mChannelCalculator = new ChannelCalculator(tunerController.getSampleRate(), channelCount,
|
||||
tunerController.getFrequency(), CHANNEL_OVERSAMPLING);
|
||||
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
|
||||
mBufferDispatcher.setListener(mNativeBufferReceiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a polyphase channel manager for the tuner controller
|
||||
*
|
||||
* @param tunerController for a tuner that provides a baseband complex buffer stream.
|
||||
*/
|
||||
public PolyphaseChannelManager(TunerController tunerController)
|
||||
{
|
||||
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a description of the state of this manager.
|
||||
*/
|
||||
@@ -201,7 +188,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
try
|
||||
{
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
|
||||
mChannelSourceEventListener, threadName);
|
||||
mChannelSourceEventListener, threadName, mTunerController.getTunerFrequencyErrorManager());
|
||||
|
||||
mChannelSources.add(channelSource);
|
||||
}
|
||||
@@ -234,7 +221,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
//If this is the first channel, register to start the sample buffers flowing
|
||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
|
||||
{
|
||||
mNativeBufferProvider.addBufferListener(mBufferDispatcher);
|
||||
mTunerController.addBufferListener(mBufferDispatcher);
|
||||
mPolyphaseChannelizer.start();
|
||||
mBufferDispatcher.start();
|
||||
}
|
||||
@@ -263,7 +250,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
//If this is the last/only channel, deregister to stop the sample buffers
|
||||
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||
{
|
||||
mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
|
||||
mTunerController.removeBufferListener(mBufferDispatcher);
|
||||
mBufferDispatcher.stop();
|
||||
mPolyphaseChannelizer.stop();
|
||||
}
|
||||
@@ -303,6 +290,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
||||
case NOTIFICATION_RECORDING_FILE_LOADED:
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||
//no-op
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
@@ -456,9 +444,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
|
||||
//Rebroadcast so that the tuner source can process this event
|
||||
mSourceEventBroadcaster.broadcast(sourceEvent);
|
||||
case REQUEST_FREQUENCY_CORRECTION:
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||
//Ignore
|
||||
break;
|
||||
default:
|
||||
mLog.error("Received unrecognized source event from polyphase channel source [" +
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
@@ -47,6 +48,8 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
private List<Integer> mOutputProcessorIndexes = new ArrayList<>();
|
||||
private double mTunerSampleRate;
|
||||
private double mTunerCenterFrequency;
|
||||
private long mFrequencyCorrection;
|
||||
private boolean mPendingFrequencyCorrectionUpdate = false;
|
||||
private PendingOutputProcessorUpdate mPendingOutputProcessorUpdate;
|
||||
|
||||
/**
|
||||
@@ -57,17 +60,27 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
* @param filterManager for access to new or cached synthesis filters
|
||||
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
|
||||
* @param threadName for the channel's dispatcher
|
||||
* @param tunerFrequencyErrorManager to connect with the channel frequency error manager
|
||||
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
|
||||
*/
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
|
||||
Listener<SourceEvent> producerSourceEventListener, String threadName)
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator,
|
||||
SynthesisFilterManager filterManager,
|
||||
Listener<SourceEvent> producerSourceEventListener, String threadName,
|
||||
TunerFrequencyErrorManager tunerFrequencyErrorManager)
|
||||
throws IllegalArgumentException
|
||||
{
|
||||
super(producerSourceEventListener, tunerChannel, threadName);
|
||||
super(producerSourceEventListener, tunerChannel, threadName, tunerFrequencyErrorManager);
|
||||
mChannelSampleRate = channelCalculator.getChannelSampleRate();
|
||||
doUpdateOutputProcessor(channelCalculator, filterManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyCorrection(long correction)
|
||||
{
|
||||
mFrequencyCorrection = correction;
|
||||
mPendingFrequencyCorrectionUpdate = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of the state or configuration of the output processor.
|
||||
*/
|
||||
@@ -280,6 +293,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
doUpdateOutputProcessor(channelCalculator, filterManager);
|
||||
}
|
||||
|
||||
//Apply channel decider requested frequency correction
|
||||
if(mPendingFrequencyCorrectionUpdate)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPendingFrequencyCorrectionUpdate = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if(mPolyphaseChannelOutputProcessor != null)
|
||||
@@ -349,7 +369,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
*/
|
||||
public long getFrequencyOffset()
|
||||
{
|
||||
return mIndexCenterFrequency - getTunerChannel().getFrequency();
|
||||
return mIndexCenterFrequency - getTunerChannel().getFrequency() + mFrequencyCorrection;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -52,6 +52,44 @@ public enum Dibit
|
||||
mPhase = phase;
|
||||
}
|
||||
|
||||
private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
|
||||
|
||||
/**
|
||||
* Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
|
||||
* @param sample in radians.
|
||||
* @return symbol decision.
|
||||
*/
|
||||
public static Dibit fromSample(float sample)
|
||||
{
|
||||
if(sample > 0)
|
||||
{
|
||||
return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the error from the sample by first mapping it to a symbol and the error is the difference from
|
||||
* the ideal phase of that symbol.
|
||||
* @param sample to process
|
||||
* @return error from ideal
|
||||
*/
|
||||
public static float getError(float sample)
|
||||
{
|
||||
if(sample > 0)
|
||||
{
|
||||
return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3.mPhase - sample : Dibit.D00_PLUS_1.mPhase - sample;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3.mPhase - sample : Dibit.D10_MINUS_1.mPhase - sample;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Binary value of the symbol
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -73,6 +73,18 @@ public class DibitDelayLine
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the dibit array into the buffer
|
||||
* @param dibits to fill
|
||||
*/
|
||||
public void fill(Dibit[] dibits)
|
||||
{
|
||||
for(Dibit dibit: dibits)
|
||||
{
|
||||
insert(dibit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the pointer to correct a dibit stuff/delete.
|
||||
* @param offset to adjust.
|
||||
@@ -110,7 +122,7 @@ public class DibitDelayLine
|
||||
public void log()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Pointer [").append(mPointer).append("] ");
|
||||
int pointer = mPointer;
|
||||
for(int i = 0; i < mDelayLine.length; i++)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.dsp.symbol;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -29,8 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
public class DibitToByteBufferAssembler implements Listener<Dibit>, IByteBufferProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DibitToByteBufferAssembler.class);
|
||||
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(DibitToByteBufferAssembler.class);
|
||||
private ByteBuffer mCurrentBuffer;
|
||||
private int mBufferSize;
|
||||
private byte mCurrentByte;
|
||||
@@ -53,14 +53,21 @@ public class DibitToByteBufferAssembler implements Listener<Dibit>, IByteBufferP
|
||||
* the buffer pointer to zero so that new dibits can be loaded.
|
||||
*/
|
||||
private void getNextBuffer()
|
||||
{
|
||||
closeAndBroadcast();
|
||||
mCurrentBuffer = ByteBuffer.allocate(mBufferSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the current buffer and broadcasts it to an optionally registered listener
|
||||
*/
|
||||
private void closeAndBroadcast()
|
||||
{
|
||||
if(mCurrentBuffer != null && mBufferListener != null)
|
||||
{
|
||||
mCurrentBuffer.flip();
|
||||
mBufferListener.receive(mCurrentBuffer);
|
||||
}
|
||||
|
||||
mCurrentBuffer = ByteBuffer.allocate(mBufferSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,17 +95,33 @@ public class DibitToByteBufferAssembler implements Listener<Dibit>, IByteBufferP
|
||||
|
||||
if(mDibitCount >= 4)
|
||||
{
|
||||
mCurrentBuffer.put(mCurrentByte);
|
||||
mCurrentByte = 0x00;
|
||||
mDibitCount = 0;
|
||||
|
||||
if(!mCurrentBuffer.hasRemaining())
|
||||
{
|
||||
getNextBuffer();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mCurrentBuffer.put(mCurrentByte);
|
||||
}
|
||||
catch (BufferOverflowException e)
|
||||
{
|
||||
//This shouldn't happen.
|
||||
LOGGER.info("Detected buffer overflow in dibit buffer assembler - continuing");
|
||||
}
|
||||
mCurrentByte = 0x00;
|
||||
mDibitCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any buffered bits to the registered listener.
|
||||
*/
|
||||
public void flush()
|
||||
{
|
||||
closeAndBroadcast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive fully assembled byte buffers from this assembler.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user