diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 67e551ce..31ec5bc1 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -4,6 +4,7 @@ esnid fleetsync ident + polyphase talkgroup tgid diff --git a/.idea/libraries/jmbe_interface_0_3_3.xml b/.idea/libraries/jmbe_interface_0_3_3.xml new file mode 100644 index 00000000..1b73f0a9 --- /dev/null +++ b/.idea/libraries/jmbe_interface_0_3_3.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/imports/jmbe-interface-0.3.2.jar b/imports/jmbe-interface-0.3.3.jar similarity index 88% rename from imports/jmbe-interface-0.3.2.jar rename to imports/jmbe-interface-0.3.3.jar index 29100a3f..73fac286 100644 Binary files a/imports/jmbe-interface-0.3.2.jar and b/imports/jmbe-interface-0.3.3.jar differ diff --git a/sdrtrunk.iml b/sdrtrunk.iml index 740623a3..4ceeed69 100644 --- a/sdrtrunk.iml +++ b/sdrtrunk.iml @@ -4,6 +4,9 @@ + + + @@ -11,11 +14,12 @@ - + + \ No newline at end of file diff --git a/src/audio/AudioFormats.java b/src/audio/AudioFormats.java index f7c58198..ac859f54 100644 --- a/src/audio/AudioFormats.java +++ b/src/audio/AudioFormats.java @@ -35,7 +35,7 @@ public class AudioFormats IMBE_FRAME_RATE, LITTLE_ENDIAN ); - public static AudioFormat PCM_SIGNED_8KHZ_16BITS = + public static AudioFormat PCM_SIGNED_8KHZ_16BITS_MONO = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, PCM_8KHZ_RATE, PCM_SAMPLE_SIZE_BITS, @@ -43,7 +43,16 @@ public class AudioFormats PCM_FRAME_SIZE_BYTES_MONO, PCM_8KHZ_RATE, LITTLE_ENDIAN ); - + + public static AudioFormat PCM_SIGNED_8KHZ_16BITS_STEREO = + new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, + PCM_8KHZ_RATE, + PCM_SAMPLE_SIZE_BITS, + TWO_CHANNELS, + PCM_FRAME_SIZE_BYTES_STEREO, + PCM_8KHZ_RATE, + LITTLE_ENDIAN ); + public static AudioFormat PCM_SIGNED_48KHZ_16BITS_MONO = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, PCM_48KHZ_RATE, @@ -66,11 +75,11 @@ public class AudioFormats * Source Data Line Info for a 48 kHz, 16-bits signed PCM one channel */ public static final Line.Info MONO_SOURCE_DATALINE_INFO = - new DataLine.Info( SourceDataLine.class, PCM_SIGNED_48KHZ_16BITS_MONO ); + new DataLine.Info( SourceDataLine.class, PCM_SIGNED_8KHZ_16BITS_MONO); /** * Source Data Line Info for a 48 kHz, 16-bits signed PCM two channels */ public static final Line.Info STEREO_SOURCE_DATALINE_INFO = - new DataLine.Info( SourceDataLine.class, PCM_SIGNED_48KHZ_16BITS_STEREO ); + new DataLine.Info( SourceDataLine.class, PCM_SIGNED_8KHZ_16BITS_STEREO); } diff --git a/src/audio/AudioModule.java b/src/audio/AudioModule.java index 01ad02d4..4a02298f 100644 --- a/src/audio/AudioModule.java +++ b/src/audio/AudioModule.java @@ -3,6 +3,10 @@ package audio; import java.util.Arrays; import java.util.concurrent.ScheduledExecutorService; +import dsp.filter.design.FilterDesignException; +import dsp.filter.fir.FIRFilterSpecification; +import dsp.filter.fir.remez.RemezFIRFilterDesigner; +import dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB; import module.Module; import org.slf4j.Logger; @@ -22,171 +26,209 @@ import dsp.filter.Filters; import dsp.filter.fir.real.RealFIRFilter_R_R; /** - * Provides packaging of demodulated audio sample buffers into audio packets for - * broadcast to registered audio packet listeners. Includes audio packet + * Provides packaging of demodulated audio sample buffers into audio packets for + * broadcast to registered audio packet listeners. Includes audio packet * metadata in constructed audio packets. - * + * * Incorporates audio squelch state listener to control if audio packets are * broadcast or ignored. */ -public class AudioModule extends Module implements IAudioPacketProvider, - IChannelEventListener, - IMetadataListener, - IFilteredRealBufferListener, - ISquelchStateListener, - Listener +public class AudioModule extends Module implements IAudioPacketProvider, + IChannelEventListener, + IMetadataListener, + IFilteredRealBufferListener, + ISquelchStateListener, + Listener { - protected static final Logger mLog = LoggerFactory.getLogger( AudioModule.class ); - - /* Provides a unique identifier for this audio module instance to use as a - * source identifier for all audio packets */ - private static int UNIQUE_ID = 0; - private int mSourceID; - - private AudioMetadata mAudioMetadata; - private ChannelEventListener mChannelEventListener = new ChannelEventListener(); - private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); - private SquelchState mSquelchState = SquelchState.SQUELCH; - private Listener mAudioPacketListener; + protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class); -//TODO: remove this -- is being replaced by the DemodulatedAudioFilterModule - private RealFIRFilter_R_R mAudioFilter = new RealFIRFilter_R_R( - Filters.FIR_BANDPASS_AUDIO_48KHZ.getCoefficients(), 2.0f ); - - public AudioModule( boolean record ) - { - mSourceID = ++UNIQUE_ID; - mAudioMetadata = new AudioMetadata( mSourceID, record ); - } - - @Override - public void dispose() - { - mSquelchStateListener = null; - mAudioPacketListener = null; - } + private static float[] mDecimationCoefficients; - @Override - public void reset() - { - mAudioMetadata.reset(); - } - - @Override - public void start( ScheduledExecutorService executor ) - { - /* No start operations provided */ - } + static + { + //Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling). + FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() + .sampleRate( 48000 ) + .stopFrequency1( 150 ) + .passFrequencyBegin( 300 ) + .passFrequencyEnd( 3500 ) + .stopFrequency2( 4000 ) + .stopRipple( 0.0004 ) + .passRipple( 0.008) + .build(); - @Override - public void stop() - { + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + try + { + if (!designer.isValid()) + { + throw new FilterDesignException("Couldn't design the audio decimation filter"); + } + + mDecimationCoefficients = designer.getImpulseResponse(); + } + catch (FilterDesignException e) + { + mLog.debug("Error designing filter", e); + } + } + + /* Provides a unique identifier for this audio module instance to use as a + * source identifier for all audio packets */ + private static int UNIQUE_ID = 0; + private int mSourceID; + + private AudioMetadata mAudioMetadata; + private ChannelEventListener mChannelEventListener = new ChannelEventListener(); + private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); + private SquelchState mSquelchState = SquelchState.SQUELCH; + private Listener mAudioPacketListener; + private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter; + + public AudioModule(boolean record) + { + mSourceID = ++UNIQUE_ID; + mAudioMetadata = new AudioMetadata(mSourceID, record); + + mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f); + mAudioDecimationFilter.setListener(new Listener() + { + @Override + public void receive(RealBuffer realBuffer) + { + if (mAudioPacketListener != null) + { + AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mAudioMetadata.copyOf()); + mAudioPacketListener.receive(packet); + } + } + }); + } + + @Override + public void dispose() + { + mSquelchStateListener = null; + mAudioPacketListener = null; + } + + @Override + public void reset() + { + mAudioMetadata.reset(); + } + + @Override + public void start(ScheduledExecutorService executor) + { + /* No start operations provided */ + } + + @Override + public void stop() + { /* Issue an end audio packet in case a recorder is still rolling */ - if( mAudioPacketListener != null ) - { - mAudioPacketListener.receive( new AudioPacket( AudioPacket.Type.END, - mAudioMetadata.copyOf() ) ); - } - } + if (mAudioPacketListener != null) + { + mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, + mAudioMetadata.copyOf())); + } + } - /** - * Processes demodulated audio samples into audio packets with current audio - * metadata and sends to the registered listener - */ - @Override - public void receive( RealBuffer buffer ) - { - if( mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH ) - { - /* We make a copy of the samples, so that we don't affect any other - * processes that might be concurrently processing the same buffer */ - float[] audio = Arrays.copyOf( buffer.getSamples(), buffer.getSamples().length ); - - mAudioFilter.filter( audio ); - - AudioPacket packet = new AudioPacket( audio, mAudioMetadata.copyOf() ); + /** + * Processes demodulated audio samples into audio packets with current audio + * metadata and sends to the registered listener + */ + @Override + public void receive(RealBuffer buffer) + { + if (mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH) + { + mAudioDecimationFilter.receive(buffer); - if( mAudioPacketListener != null ) - { - mAudioPacketListener.receive( packet ); - } - } - } +// /* We make a copy of the samples, so that we don't affect any other +// * processes that might be concurrently processing the same buffer */ +// float[] audio = Arrays.copyOf( buffer.getSamples(), buffer.getSamples().length ); +// +// mAudioFilter.filter( audio ); + } + } - @Override - public Listener getFilteredRealBufferListener() - { - return this; - } + @Override + public Listener getFilteredRealBufferListener() + { + return this; + } - @Override - public Listener getMetadataListener() - { - return mAudioMetadata; - } + @Override + public Listener getMetadataListener() + { + return mAudioMetadata; + } - @Override - public void setAudioPacketListener( Listener listener ) - { - mAudioPacketListener = listener; - } + @Override + public void setAudioPacketListener(Listener listener) + { + mAudioPacketListener = listener; + } - @Override - public void removeAudioPacketListener() - { - mAudioPacketListener = null; - } + @Override + public void removeAudioPacketListener() + { + mAudioPacketListener = null; + } - @Override - public Listener getChannelEventListener() - { - return mChannelEventListener; - } + @Override + public Listener getChannelEventListener() + { + return mChannelEventListener; + } - @Override - public Listener getSquelchStateListener() - { - return mSquelchStateListener; - } + @Override + public Listener getSquelchStateListener() + { + return mSquelchStateListener; + } - /** - * Wrapper for channel event listener. Responds to channel state reset - * events to remove/cleanup current audio metadata - */ - public class ChannelEventListener implements Listener - { - @Override - public void receive( ChannelEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_STATE_RESET: - mAudioMetadata.reset(); - break; - case NOTIFICATION_SELECTION_CHANGE: - mAudioMetadata.setSelected( event.getChannel().isSelected() ); - break; - default: - break; - } - } - } + /** + * Wrapper for channel event listener. Responds to channel state reset + * events to remove/cleanup current audio metadata + */ + public class ChannelEventListener implements Listener + { + @Override + public void receive(ChannelEvent event) + { + switch (event.getEvent()) + { + case NOTIFICATION_STATE_RESET: + mAudioMetadata.reset(); + break; + case NOTIFICATION_SELECTION_CHANGE: + mAudioMetadata.setSelected(event.getChannel().isSelected()); + break; + default: + break; + } + } + } - /** - * Wrapper for squelch state listener - */ - public class SquelchStateListener implements Listener - { - @Override - public void receive( SquelchState state ) - { - if( state == SquelchState.SQUELCH && mAudioPacketListener != null ) - { - mAudioPacketListener.receive( new AudioPacket( AudioPacket.Type.END, - mAudioMetadata.copyOf() ) ); - } - - mSquelchState = state; - } - } + /** + * Wrapper for squelch state listener + */ + public class SquelchStateListener implements Listener + { + @Override + public void receive(SquelchState state) + { + if (state == SquelchState.SQUELCH && mAudioPacketListener != null) + { + mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, + mAudioMetadata.copyOf())); + } + + mSquelchState = state; + } + } } diff --git a/src/audio/AudioPacket.java b/src/audio/AudioPacket.java index 12bbf22b..2936557e 100644 --- a/src/audio/AudioPacket.java +++ b/src/audio/AudioPacket.java @@ -5,7 +5,7 @@ import audio.metadata.AudioMetadata; /******************************************************************************* * SDR Trunk - * Copyright (C) 2014,2015 Dennis Sheirer + * Copyright (C) 2014,2016 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 diff --git a/src/audio/output/MonoAudioOutput.java b/src/audio/output/MonoAudioOutput.java index e146e507..a217c192 100644 --- a/src/audio/output/MonoAudioOutput.java +++ b/src/audio/output/MonoAudioOutput.java @@ -33,14 +33,14 @@ import controller.ThreadPoolManager; */ public class MonoAudioOutput extends AudioOutput { - private final static int BUFFER_SIZE = 48000; + private final static int BUFFER_SIZE = 8000; public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer ) { super( threadPoolManager, mixer, MixerChannel.MONO, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE ); } diff --git a/src/audio/output/StereoAudioOutput.java b/src/audio/output/StereoAudioOutput.java index 2ba8db41..098112ec 100644 --- a/src/audio/output/StereoAudioOutput.java +++ b/src/audio/output/StereoAudioOutput.java @@ -33,7 +33,7 @@ import controller.ThreadPoolManager; */ public class StereoAudioOutput extends AudioOutput { - private final static int BUFFER_SIZE = 96000; + private final static int BUFFER_SIZE = 16000; public StereoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer, MixerChannel channel ) @@ -41,7 +41,7 @@ public class StereoAudioOutput extends AudioOutput super( threadPoolManager, mixer, channel, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, + AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO, BUFFER_SIZE ); } diff --git a/src/dsp/filter/design/FilterView.css b/src/dsp/filter/design/FilterView.css index 9c70eec8..43b2bf73 100644 --- a/src/dsp/filter/design/FilterView.css +++ b/src/dsp/filter/design/FilterView.css @@ -1,12 +1,12 @@ /* ....Show License.... */ .chart { - -fx-background-image: url("../../../../images/Curve-fitted-background.png"); + -fx-background-image: url("../../../Curve-fitted-background.png"); -fx-padding: 15 25 15 15; } .chart-plot-background { -fx-background-color: red; - -fx-background-image: url("../../../../images/Curve-fitted-chart-background.png"), - url("../../../../images/Curve-fitted-graph-gridlines.png"); + -fx-background-image: url("../../../Curve-fitted-chart-background.png"), + url("../../../Curve-fitted-graph-gridlines.png"); -fx-background-size: cover, auto; -fx-background-repeat: no-repeat, repeat; -fx-background-position: 0% 0%, 0% 100%; diff --git a/src/dsp/filter/design/FilterViewer.java b/src/dsp/filter/design/FilterViewer.java index 7dac65b7..3b84c4a4 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -40,37 +40,35 @@ public class FilterViewer extends Application */ private float[] getFilter() { - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate( 10000 ) - .gridDensity( 16 ) - .passBandCutoff( 1300 ) +// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() +// .sampleRate( 48000 ) +// .gridDensity( 16 ) +// .passBandCutoff( 3000 ) +// .passBandAmplitude( 1.0 ) +// .passBandRipple( 0.01 ) +// .stopBandStart( 4000 ) +// .stopBandAmplitude( 0.0 ) +// .stopBandRipple( 0.027 ) +// .build(); + + FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() + .sampleRate( 8000 ) + .stopBandCutoff( 250 ) + .stopBandAmplitude( 0.0 ) + .stopBandRipple( 0.01 ) + .passBandStart( 300 ) .passBandAmplitude( 1.0 ) .passBandRipple( 0.01 ) - .stopBandStart( 2675 ) - .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.001 ) .build(); - -// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() -// .sampleRate( 8000 ) -// .gridDensity( 16 ) -// .stopBandCutoff( 2000 ) -// .stopBandAmplitude( 0.0 ) -// .stopBandRipple( 0.003 ) -// .passBandStart( 3000 ) -// .passBandAmplitude( 1.0 ) -// .passBandRipple( 0.001 ) -// .build(); // FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder() -// .sampleRate( 8000 ) -// .order( 164 ) -// .stopFrequency1( 1100 ) -// .passFrequencyBegin( 1200 ) -// .passFrequencyEnd( 2400 ) -// .stopFrequency2( 2500 ) -// .stopRipple( 0.01 ) -// .passRipple( 0.01 ) +// .sampleRate( 48000 ) +// .stopFrequency1( 150 ) +// .passFrequencyBegin( 200 ) +// .passFrequencyEnd( 3950 ) +// .stopFrequency2( 4000 ) +// .stopRipple( 0.0005 ) +// .passRipple( 0.003) // .build(); float[] taps = null; diff --git a/src/dsp/filter/polyphase/FilterStage.java b/src/dsp/filter/polyphase/FilterStage.java new file mode 100644 index 00000000..55a8556c --- /dev/null +++ b/src/dsp/filter/polyphase/FilterStage.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + ******************************************************************************/ +package dsp.filter.polyphase; + +public class FilterStage +{ + private float[] mBuffer; + private int mBufferSize; + private int mBufferPointer; + private int[][] mIndexMap; + private float[] mCoefficients; + + /** + * Single stage of a polyphase filter implemented with a circular sample buffer that uses an index map to ensure + * correct alignment of samples to filter taps. + * + * @param coefficients + */ + public FilterStage(float[] coefficients) + { + mCoefficients = coefficients; + + mBufferSize = mCoefficients.length; + + mBuffer = new float[mBufferSize]; + + //Note: buffer pointer starts at an illegal index but is decremented before first usage + mBufferPointer = mBufferSize; + + generateIndexMap(mBufferSize); + } + + /** + * Coefficients used by this filter stage. + */ + public float[] getCoefficients() + { + return mCoefficients; + } + + /** + * Cleanup prior to disposal + */ + public void dispose() + { + mCoefficients = null; + mIndexMap = null; + mBuffer = null; + } + + /** + * Loads the sample into the sample buffer + */ + private void load(float sample) + { + mBufferPointer--; + + if (mBufferPointer < 0) + { + mBufferPointer += mBufferSize; + } + + mBuffer[mBufferPointer] = sample; + } + + /** + * Applies convolution against the currently loaded buffer samples. + */ + public float filter(float sample) + { + load(sample); + + float accumulator = 0.0f; + + for (int x = 0; x < mBufferSize; x++) + { + accumulator += mCoefficients[x] * mBuffer[mIndexMap[mBufferPointer][x]]; + } + + return accumulator; + } + + /** + * Generates a circular buffer index map to support lookup of the translated index based on the current buffer + * pointer and the desired sample index. + */ + private void generateIndexMap(int size) + { + mIndexMap = new int[size][size]; + + for (int x = 0; x < size; x++) + { + for (int y = 0; y < size; y++) + { + int z = x + y; + + mIndexMap[x][y] = z < size ? z : z - size; + } + } + } +} \ No newline at end of file diff --git a/src/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java b/src/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java new file mode 100644 index 00000000..b312cad2 --- /dev/null +++ b/src/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + ******************************************************************************/ +package dsp.filter.polyphase; + +import dsp.filter.design.FilterDesignException; +import dsp.filter.fir.FIRFilter; +import dsp.filter.fir.FIRFilterSpecification; +import dsp.filter.fir.remez.RemezFIRFilterDesigner; +import dsp.mixer.Oscillator; +import gui.SDRTrunk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.real.RealBuffer; +import sample.real.RealToRealBufferAssembler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PolyphaseFIRDecimatingFilter_RB extends FIRFilter implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger( PolyphaseFIRDecimatingFilter_RB.class ); + + private List mFilterStages = new ArrayList<>(); + private int mFilterStagePointer; + private Listener mListener; + private RealToRealBufferAssembler mAssembler; + private int mDecimationRatio; + private float mAccumulator = 0.0f; + private float mGain = 1.0f; + + /** + * Polyphase filter stage for decimating real-valued sample buffers by the decimation ratio. Creates a number of + * polyphase filter arms/stages equal to the decimation ratio. + * + * Note: output real-valued buffer sizes are determined by the first input buffer that is presented to the filter + * where the output buffer size is equal to the floor of the input buffer size divided by the decimation ratio. + * + * @param coefficients of a low pass filter designed for the input sample rate with a cutoff frequency that conforms + * to the nyquist frequency for an output sample rate equal to the input sample rate divided by the decimation ratio. + * + * @param decimationRatio to decimate the input sample rate by. + * @param gain to apply to the filtered output. Use a value of 1.0 to apply no gain. + */ + public PolyphaseFIRDecimatingFilter_RB(float[] coefficients, int decimationRatio, float gain) + { + mDecimationRatio = decimationRatio; + mGain = gain; + + createFilterStages(coefficients, mDecimationRatio); + mFilterStagePointer = mFilterStages.size() - 1; + } + + /** + * Creates polyphase filter stages (arms) from the set of coefficients. + * + * @param coefficients for the low-pass filter + * @param decimationRatio to determine the number of polyphase filter stages. + */ + private void createFilterStages(float[] coefficients, int decimationRatio) + { + int tapSize = (int)Math.ceil((double)coefficients.length / (double)decimationRatio); + + float[][] coefficientSets = new float[decimationRatio][tapSize]; + + int stagePointer = 0; + int coefficientPointer = 0; + + //Split the coefficients up into polyphase stage coefficient sets + for(int x = 0; x < coefficients.length; x++) + { + coefficientSets[stagePointer++][coefficientPointer] = coefficients[x]; + + if(stagePointer >= decimationRatio) + { + stagePointer = 0; + coefficientPointer++; + } + } + + //Create the stages + for(int x = 0; x < decimationRatio; x++) + { + mFilterStages.add(new FilterStage(coefficientSets[x])); + } + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive(RealBuffer buffer) + { + if(mAssembler == null) + { + mAssembler = new RealToRealBufferAssembler(buffer.getSamples().length / mDecimationRatio); + mAssembler.setListener(mListener); + } + + for(float sample: buffer.getSamples()) + { + filter(sample); + } + + int a = 0; + } + + private void filter(float sample) + { + FilterStage stage = mFilterStages.get(mFilterStagePointer--); + + mAccumulator += stage.filter(sample); + + if(mFilterStagePointer < 0) + { + mAssembler.receive(mAccumulator * mGain); + mAccumulator = 0.0f; + + mFilterStagePointer = mFilterStages.size() - 1; + } + } + + public void setListener(Listener listener) + { + mListener = listener; + + if(mAssembler != null) + { + mAssembler.setListener(mListener); + } + } + + public void removeListener() + { + mListener = null; + + if(mAssembler != null) + { + mAssembler.setListener(null); + } + } + + public static void main(String[] args) + { + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate( 48000 ) + .gridDensity( 16 ) + .passBandCutoff( 3000 ) + .passBandAmplitude( 1.0 ) + .passBandRipple( 0.01 ) + .stopBandStart( 4000 ) + .stopBandAmplitude( 0.0 ) + .stopBandRipple( 0.027 ) + .build(); + + RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification); + + try + { + if(!designer.isValid()) + { + throw new FilterDesignException("Couldn't design the filter"); + } + + float[] coefficients = designer.getImpulseResponse(); + PolyphaseFIRDecimatingFilter_RB filter = new PolyphaseFIRDecimatingFilter_RB(coefficients, 6, 1.0f); + filter.setListener(new Listener() + { + @Override + public void receive(RealBuffer realBuffer) + { + mLog.info("Filtered: " + Arrays.toString(realBuffer.getSamples())); + } + }); + + Oscillator oscillator = new Oscillator(3400, 48000); + + float[] samples = new float[500]; + for(int x = 0; x < 500; x++) + { + samples[x] = oscillator.getFloat(); + oscillator.rotate(); + } + + RealBuffer buffer = new RealBuffer(samples); + filter.receive(buffer); + } + catch(FilterDesignException e) + { + mLog.debug("Error designing filter", e); + } + + } +} diff --git a/src/dsp/filter/PolyphaseFIRInterpolatingFilter.java b/src/dsp/filter/polyphase/PolyphaseFIRInterpolatingFilter.java similarity index 84% rename from src/dsp/filter/PolyphaseFIRInterpolatingFilter.java rename to src/dsp/filter/polyphase/PolyphaseFIRInterpolatingFilter.java index 00c76261..65b41b46 100644 --- a/src/dsp/filter/PolyphaseFIRInterpolatingFilter.java +++ b/src/dsp/filter/polyphase/PolyphaseFIRInterpolatingFilter.java @@ -1,4 +1,22 @@ -package dsp.filter; +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 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 + * + ******************************************************************************/ +package dsp.filter.polyphase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/module/decode/ltrnet/LTRNetDecoderState.java b/src/module/decode/ltrnet/LTRNetDecoderState.java index 6f17d77a..56b0df90 100644 --- a/src/module/decode/ltrnet/LTRNetDecoderState.java +++ b/src/module/decode/ltrnet/LTRNetDecoderState.java @@ -1,17 +1,17 @@ /******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 Dennis Sheirer - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ @@ -43,747 +43,755 @@ import audio.metadata.MetadataType; public class LTRNetDecoderState extends DecoderState { - private final static Logger mLog = LoggerFactory.getLogger( LTRNetDecoderState.class ); + private final static Logger mLog = LoggerFactory.getLogger(LTRNetDecoderState.class); - private DecimalFormat mDecimalFormatter = new DecimalFormat( "0.00000" ); + private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); - private TreeSet mTalkgroups = new TreeSet(); - private TreeSet mTalkgroupsFirstHeard = new TreeSet(); - private TreeSet mESNs = new TreeSet(); - private TreeSet mUniqueIDs = new TreeSet(); - private TreeSet mNeighborIDs = new TreeSet(); - private TreeSet mSiteIDs = new TreeSet(); - private HashMap mReceiveFrequencies = - new HashMap(); - private HashMap mTransmitFrequencies = - new HashMap(); - private HashMap mActiveCalls = new HashMap(); + private TreeSet mTalkgroups = new TreeSet(); + private TreeSet mTalkgroupsFirstHeard = new TreeSet(); + private TreeSet mESNs = new TreeSet(); + private TreeSet mUniqueIDs = new TreeSet(); + private TreeSet mNeighborIDs = new TreeSet(); + private TreeSet mSiteIDs = new TreeSet(); + private HashMap mReceiveFrequencies = + new HashMap(); + private HashMap mTransmitFrequencies = + new HashMap(); + private HashMap mActiveCalls = new HashMap(); - private String mTalkgroup; - private Alias mTalkgroupAlias; - private String mDescription; - private int mChannelNumber; - private long mFrequency = 0; + private String mTalkgroup; + private Alias mTalkgroupAlias; + private String mDescription; + private int mChannelNumber; + private long mFrequency = 0; - public LTRNetDecoderState( AliasList aliasList ) - { - super( aliasList ); - } - - @Override - public DecoderType getDecoderType() - { - return DecoderType.LTR_NET; - } - - @Override - public void receive( Message message ) + public LTRNetDecoderState(AliasList aliasList) { - if( message.isValid() ) - { - State state = State.IDLE; + super(aliasList); + } - if( message instanceof LTRNetOSWMessage ) - { - LTRNetOSWMessage ltr = (LTRNetOSWMessage)message; - - switch( ltr.getMessageType() ) - { - case CA_ENDD: - if( mChannelNumber == 0 ) - { - mChannelNumber = ltr.getChannel(); - broadcast( ChangedAttribute.CHANNEL_NUMBER ); - } + @Override + public DecoderType getDecoderType() + { + return DecoderType.LTR_NET; + } + + @Override + public void receive(Message message) + { + if (message.isValid()) + { + State state = State.IDLE; + + if (message instanceof LTRNetOSWMessage) + { + LTRNetOSWMessage ltr = (LTRNetOSWMessage) message; + + switch (ltr.getMessageType()) + { + case CA_ENDD: + if (mChannelNumber == 0) + { + mChannelNumber = ltr.getChannel(); + broadcast(ChangedAttribute.CHANNEL_NUMBER); + } /* Process FCC Station ID Events */ - if( ltr.getGroup() == 254 ) - { - if( mCurrentCallEvent == null || - mCurrentCallEvent.getCallEventType() != - CallEventType.STATION_ID ) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.STATION_ID ) - .aliasList( getAliasList() ) - .channel( String.valueOf( mChannelNumber ) ) - .frequency( mFrequency ) - .to( ltr.getTalkgroupID() ) - .build(); + if (ltr.getGroup() == 254) + { + if (mCurrentCallEvent == null || + mCurrentCallEvent.getCallEventType() != + CallEventType.STATION_ID) + { + mCurrentCallEvent = new LTRCallEvent.Builder( + DecoderType.LTR_NET, CallEventType.STATION_ID) + .aliasList(getAliasList()) + .channel(String.valueOf(mChannelNumber)) + .frequency(mFrequency) + .to(ltr.getTalkgroupID()) + .build(); - broadcast( mCurrentCallEvent ); - - broadcast( new DecoderStateEvent( this, - Event.START, State.DATA ) ); - } - else - { - broadcast( new DecoderStateEvent( this, - Event.CONTINUATION, State.DATA ) ); - } - } - else - { - processCallEndMessage( ltr ); - } - break; - case CA_STRT: - if( mChannelNumber == 0 ) - { - mChannelNumber = ltr.getChannel(); - broadcast( ChangedAttribute.CHANNEL_NUMBER ); - } + broadcast(mCurrentCallEvent); + + broadcast(new DecoderStateEvent(this, + Event.START, State.DATA)); + } + else + { + broadcast(new DecoderStateEvent(this, + Event.CONTINUATION, State.DATA)); + } + } + else + { + processCallEndMessage(ltr); + } + break; + case CA_STRT: + if (mChannelNumber == 0) + { + mChannelNumber = ltr.getChannel(); + broadcast(ChangedAttribute.CHANNEL_NUMBER); + } /* If the call event channel matches our current channel - * then it's a call, otherwise it's a call detect. */ - if( ltr.getChannel() == mChannelNumber ) - { - processCallMessage( ltr ); - } - else - { - processCallDetectMessage( ltr ); - } - - break; - case SY_IDLE: - if( mChannelNumber != ltr.getChannel() ) - { - mChannelNumber = ltr.getChannel(); - broadcast( ChangedAttribute.CHANNEL_NUMBER ); - } - break; - case MA_CHNH: - break; - case MA_CHNL: - break; - case FQ_RXHI: - case FQ_RXLO: - if( ltr.getFrequency() > 0 ) - { - mReceiveFrequencies.put( ltr.getHomeRepeater(), - ltr.getFrequency() ); - } - break; - case FQ_TXHI: - case FQ_TXLO: - if( ltr.getFrequency() > 0 ) - { - mTransmitFrequencies.put( ltr.getHomeRepeater(), - ltr.getFrequency() ); - } - break; - case ID_NBOR: - String neighborID = ltr.getNeighborID(); - - if( neighborID != null ) - { - mNeighborIDs.add( neighborID ); - } - break; - case ID_UNIQ: - state = State.DATA; - - int uniqueID = ltr.getRadioUniqueID(); - - if( uniqueID != LTRNetOSWMessage.INT_NULL_VALUE ) - { - mUniqueIDs.add( uniqueID ); - } - - if( getCurrentLTRCallEvent() == null ) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.REGISTER ) - .aliasList( getAliasList() ) - .channel( String.valueOf( mChannelNumber ) ) - .frequency( mFrequency ) - .from( String.valueOf( uniqueID ) ) - .build(); - } - else - { - mCurrentCallEvent.setFromID( - String.valueOf( ltr.getRadioUniqueID() ) ); - mCurrentCallEvent.setDetails( "Unique ID" ); - - broadcast( mCurrentCallEvent ); - } + * then it's a call, otherwise it's a call detect. */ + if (ltr.getChannel() == mChannelNumber) + { + processCallMessage(ltr); + } + else + { + processCallDetectMessage(ltr); + } - mTalkgroup = String.valueOf( uniqueID ); - broadcast( ChangedAttribute.TO_TALKGROUP ); - - mTalkgroupAlias = ltr.getRadioUniqueIDAlias(); - broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); - break; - case ID_SITE: - String siteID = ltr.getSiteID(); - - if( siteID != null ) - { - mSiteIDs.add( siteID ); - } - break; - default: - break; - } - } - else if( message instanceof LTRNetISWMessage ) - { - LTRNetISWMessage ltr = ((LTRNetISWMessage)message); - - switch( ltr.getMessageType() ) - { - case CA_STRT: - processCallMessage( ltr ); - break; - case CA_ENDD: - processCallEndMessage( ltr ); - break; - case ID_ESNH: - case ID_ESNL: - state = State.DATA; - - String esn = ltr.getESN(); - - if( !esn.contains( "xxxx" ) ) - { - mESNs.add( ltr.getESN() ); - } - - mDescription = "ESN"; - broadcast( ChangedAttribute.DESCRIPTION ); - - mTalkgroup = ltr.getESN(); - broadcast( ChangedAttribute.TO_TALKGROUP ); + break; + case SY_IDLE: + if (mChannelNumber != ltr.getChannel()) + { + mChannelNumber = ltr.getChannel(); + broadcast(ChangedAttribute.CHANNEL_NUMBER); + } + break; + case MA_CHNH: + break; + case MA_CHNL: + break; + case FQ_RXHI: + case FQ_RXLO: + if (ltr.getFrequency() > 0) + { + mReceiveFrequencies.put(ltr.getHomeRepeater(), + ltr.getFrequency()); + } + break; + case FQ_TXHI: + case FQ_TXLO: + if (ltr.getFrequency() > 0) + { + mTransmitFrequencies.put(ltr.getHomeRepeater(), + ltr.getFrequency()); + } + break; + case ID_NBOR: + String neighborID = ltr.getNeighborID(); - mTalkgroupAlias = ltr.getESNAlias(); - broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); + if (neighborID != null) + { + mNeighborIDs.add(neighborID); + } + break; + case ID_UNIQ: + state = State.DATA; - broadcast( new DecoderStateEvent( this, Event.DECODE, State.DATA ) ); + int uniqueID = ltr.getRadioUniqueID(); - if( mCurrentCallEvent == null ) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.REGISTER_ESN ) - .aliasList( getAliasList() ) - .details( "ESN:" + ltr.getESN() ) - .frequency( mFrequency ) - .from( ltr.getESN() ) - .build(); - - broadcast( mCurrentCallEvent ); - } - - break; - case ID_UNIQ: - state = State.DATA; - - int uniqueid = ltr.getRadioUniqueID(); - - if( uniqueid != LTRNetISWMessage.INT_NULL_VALUE ) - { - mUniqueIDs.add( uniqueid ); - - mDescription = "REGISTER UID"; - broadcast( ChangedAttribute.DESCRIPTION ); + if (uniqueID != LTRNetOSWMessage.INT_NULL_VALUE) + { + mUniqueIDs.add(uniqueID); + } - mTalkgroup = String.valueOf( uniqueid ); - broadcast( ChangedAttribute.TO_TALKGROUP ); + if (getCurrentLTRCallEvent() == null) + { + mCurrentCallEvent = new LTRCallEvent.Builder( + DecoderType.LTR_NET, CallEventType.REGISTER) + .aliasList(getAliasList()) + .channel(String.valueOf(mChannelNumber)) + .frequency(mFrequency) + .from(String.valueOf(uniqueID)) + .build(); + } + else + { + mCurrentCallEvent.setFromID( + String.valueOf(ltr.getRadioUniqueID())); + mCurrentCallEvent.setDetails("Unique ID"); - mTalkgroupAlias = ltr.getRadioUniqueIDAlias(); - broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); - - if( getCurrentLTRCallEvent() == null ) - { - mCurrentCallEvent = new LTRCallEvent.Builder( - DecoderType.LTR_NET, CallEventType.REGISTER ) - .aliasList( getAliasList() ) - .channel( String.valueOf( mChannelNumber ) ) - .frequency( mFrequency ) - .from( String.valueOf( uniqueid ) ) - .build(); - } - else - { - mCurrentCallEvent.setFromID( - String.valueOf( ltr.getRadioUniqueID() ) ); - mCurrentCallEvent.setDetails( "Unique ID" ); - - broadcast( mCurrentCallEvent ); - } - } - break; - default: - break; - } - } - } + broadcast(mCurrentCallEvent); + } + + mTalkgroup = String.valueOf(uniqueID); + broadcast(ChangedAttribute.TO_TALKGROUP); + + mTalkgroupAlias = ltr.getRadioUniqueIDAlias(); + broadcast(ChangedAttribute.TO_TALKGROUP_ALIAS); + break; + case ID_SITE: + String siteID = ltr.getSiteID(); + + if (siteID != null) + { + mSiteIDs.add(siteID); + } + break; + default: + break; + } + } + else if (message instanceof LTRNetISWMessage) + { + LTRNetISWMessage ltr = ((LTRNetISWMessage) message); + + switch (ltr.getMessageType()) + { + case CA_STRT: + processCallMessage(ltr); + break; + case CA_ENDD: + processCallEndMessage(ltr); + break; + case ID_ESNH: + case ID_ESNL: + state = State.DATA; + + String esn = ltr.getESN(); + + if (!esn.contains("xxxx")) + { + mESNs.add(ltr.getESN()); + } + + mDescription = "ESN"; + broadcast(ChangedAttribute.DESCRIPTION); + + mTalkgroup = ltr.getESN(); + broadcast(ChangedAttribute.TO_TALKGROUP); + + mTalkgroupAlias = ltr.getESNAlias(); + broadcast(ChangedAttribute.TO_TALKGROUP_ALIAS); + + broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA)); + + if (mCurrentCallEvent == null) + { + mCurrentCallEvent = new LTRCallEvent.Builder( + DecoderType.LTR_NET, CallEventType.REGISTER_ESN) + .aliasList(getAliasList()) + .details("ESN:" + ltr.getESN()) + .frequency(mFrequency) + .from(ltr.getESN()) + .build(); + + broadcast(mCurrentCallEvent); + } + + break; + case ID_UNIQ: + state = State.DATA; + + int uniqueid = ltr.getRadioUniqueID(); + + if (uniqueid != LTRNetISWMessage.INT_NULL_VALUE) + { + mUniqueIDs.add(uniqueid); + + mDescription = "REGISTER UID"; + broadcast(ChangedAttribute.DESCRIPTION); + + mTalkgroup = String.valueOf(uniqueid); + broadcast(ChangedAttribute.TO_TALKGROUP); + + mTalkgroupAlias = ltr.getRadioUniqueIDAlias(); + broadcast(ChangedAttribute.TO_TALKGROUP_ALIAS); + + if (getCurrentLTRCallEvent() == null) + { + mCurrentCallEvent = new LTRCallEvent.Builder( + DecoderType.LTR_NET, CallEventType.REGISTER) + .aliasList(getAliasList()) + .channel(String.valueOf(mChannelNumber)) + .frequency(mFrequency) + .from(String.valueOf(uniqueid)) + .build(); + } + else + { + mCurrentCallEvent.setFromID( + String.valueOf(ltr.getRadioUniqueID())); + mCurrentCallEvent.setDetails("Unique ID"); + + broadcast(mCurrentCallEvent); + } + } + break; + default: + break; + } + } + } } - - public LTRCallEvent getCurrentLTRCallEvent() - { - if( mCurrentCallEvent != null ) - { - return (LTRCallEvent)mCurrentCallEvent; - } - - return null; - } - - @Override + + public LTRCallEvent getCurrentLTRCallEvent() + { + if (mCurrentCallEvent != null) + { + return (LTRCallEvent) mCurrentCallEvent; + } + + return null; + } + + @Override public String getActivitySummary() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); - sb.append( "Activity Summary\n" ); - sb.append( "Decoder:\tLTR-Net\n\n" ); + sb.append("Activity Summary\n"); + sb.append("Decoder:\tLTR-Net\n\n"); - if( mSiteIDs.isEmpty() ) - { - sb.append( "Site:\tUnknown\n" ); - } - else - { - Iterator it = mSiteIDs.iterator(); - - while( it.hasNext() ) - { - sb.append( "Site:\t" ); - - String siteID = it.next(); + if (mSiteIDs.isEmpty()) + { + sb.append("Site:\tUnknown\n"); + } + else + { + Iterator it = mSiteIDs.iterator(); - sb.append( siteID ); - - if( hasAliasList() ) - { - Alias siteAlias = getAliasList().getSiteID( String.valueOf( siteID ) ); + while (it.hasNext()) + { + sb.append("Site:\t"); - if( siteAlias != null ) - { - sb.append( " " ); - sb.append( siteAlias.getName() ); - } - } + String siteID = it.next(); - sb.append( "\n" ); - } - } - - sb.append( "\nLCNs (transmit | receive)\n" ); - - if( mReceiveFrequencies.isEmpty() && mTransmitFrequencies.isEmpty() ) - { - sb.append( " None\n" ); - } - else - { - for( int x = 1; x < 21; x++ ) - { - long rcv = 0; - - if( mReceiveFrequencies.containsKey( x ) ) - { - rcv = mReceiveFrequencies.get( x ); - } - - long xmt = 0; - - if( mTransmitFrequencies.containsKey( x ) ) - { - xmt = mTransmitFrequencies.get( x ); - } + sb.append(siteID); - if( rcv > 0 || xmt > 0 ) - { - if( x < 10 ) - { - sb.append( " " ); - } + if (hasAliasList()) + { + Alias siteAlias = getAliasList().getSiteID(String.valueOf(siteID)); - sb.append( x ); - sb.append( ": " ); + if (siteAlias != null) + { + sb.append(" "); + sb.append(siteAlias.getName()); + } + } - if( xmt == 0 ) - { - sb.append( "---.-----" ); - } - else - { - sb.append( mDecimalFormatter.format( (double)xmt/1E6d ) ); - } - sb.append( " | " ); - - if( rcv == 0 ) - { - sb.append( "---.-----" ); - } - else - { - sb.append( mDecimalFormatter.format( (double)rcv/1E6d ) ); - } - - if( x == mChannelNumber ) - { - sb.append( " **" ); - } - - sb.append( "\n" ); - } - } - } - - sb.append( "\nTalkgroups\n" ); - - if( mTalkgroups.isEmpty() ) - { - sb.append( " None\n" ); - } - else - { - Iterator it = mTalkgroups.iterator(); - - while( it.hasNext() ) - { - String tgid = it.next(); - - sb.append( " " ); - sb.append( tgid ); - sb.append( " " ); - - if( hasAliasList() ) - { - Alias alias = getAliasList().getTalkgroupAlias( tgid ); - - if( alias != null ) - { - sb.append( alias.getName() ); - } - } - - sb.append( "\n" ); - } - } - - sb.append( "\nRadio Unique IDs\n" ); - - if( mUniqueIDs.isEmpty() ) - { - sb.append( " None\n" ); - } - else - { - Iterator it = mUniqueIDs.iterator(); - - while( it.hasNext() ) - { - int uid = it.next(); - - sb.append( " " ); - sb.append( uid ); - sb.append( " " ); - - if( hasAliasList() ) - { - Alias alias = getAliasList().getUniqueID( uid ); - - if( alias != null ) - { - sb.append( alias.getName() ); - } - } - - sb.append( "\n" ); - } - } + sb.append("\n"); + } + } - sb.append( "\nESNs\n" ); - - if( mESNs.isEmpty() ) - { - sb.append( " None\n" ); - } - else - { - Iterator it = mESNs.iterator(); - - while( it.hasNext() ) - { - String esn = it.next(); - - sb.append( " " ); - sb.append( esn ); - sb.append( " " ); - - if( hasAliasList() ) - { - Alias alias = getAliasList().getESNAlias( esn ); - - if( alias != null ) - { - sb.append( alias.getName() ); - } - } - - sb.append( "\n" ); - } - } - - sb.append( "\nNeighbor Sites\n" ); - - if( mNeighborIDs.isEmpty() ) - { - sb.append( " None\n" ); - } - else - { - Iterator it = mNeighborIDs.iterator(); - - while( it.hasNext() ) - { - String neighbor = it.next(); - - sb.append( " " ); - sb.append( neighbor ); - sb.append( " " ); - - if( hasAliasList() ) - { - Alias alias = getAliasList().getSiteID( String.valueOf( neighbor ) ); - - if( alias != null ) - { - sb.append( alias.getName() ); - } - } - - sb.append( "\n" ); - } - } - - return sb.toString(); + sb.append("\nLCNs (transmit | receive)\n"); + + if (mReceiveFrequencies.isEmpty() && mTransmitFrequencies.isEmpty()) + { + sb.append(" None\n"); + } + else + { + for (int x = 1; x < 21; x++) + { + long rcv = 0; + + if (mReceiveFrequencies.containsKey(x)) + { + rcv = mReceiveFrequencies.get(x); + } + + long xmt = 0; + + if (mTransmitFrequencies.containsKey(x)) + { + xmt = mTransmitFrequencies.get(x); + } + + if (rcv > 0 || xmt > 0) + { + if (x < 10) + { + sb.append(" "); + } + + sb.append(x); + sb.append(": "); + + if (xmt == 0) + { + sb.append("---.-----"); + } + else + { + sb.append(mDecimalFormatter.format((double) xmt / 1E6d)); + } + sb.append(" | "); + + if (rcv == 0) + { + sb.append("---.-----"); + } + else + { + sb.append(mDecimalFormatter.format((double) rcv / 1E6d)); + } + + if (x == mChannelNumber) + { + sb.append(" **"); + } + + sb.append("\n"); + } + } + } + + sb.append("\nTalkgroups\n"); + + if (mTalkgroups.isEmpty()) + { + sb.append(" None\n"); + } + else + { + Iterator it = mTalkgroups.iterator(); + + while (it.hasNext()) + { + String tgid = it.next(); + + sb.append(" "); + sb.append(tgid); + sb.append(" "); + + if (hasAliasList()) + { + Alias alias = getAliasList().getTalkgroupAlias(tgid); + + if (alias != null) + { + sb.append(alias.getName()); + } + } + + sb.append("\n"); + } + } + + sb.append("\nRadio Unique IDs\n"); + + if (mUniqueIDs.isEmpty()) + { + sb.append(" None\n"); + } + else + { + Iterator it = mUniqueIDs.iterator(); + + while (it.hasNext()) + { + int uid = it.next(); + + sb.append(" "); + sb.append(uid); + sb.append(" "); + + if (hasAliasList()) + { + Alias alias = getAliasList().getUniqueID(uid); + + if (alias != null) + { + sb.append(alias.getName()); + } + } + + sb.append("\n"); + } + } + + sb.append("\nESNs\n"); + + if (mESNs.isEmpty()) + { + sb.append(" None\n"); + } + else + { + Iterator it = mESNs.iterator(); + + while (it.hasNext()) + { + String esn = it.next(); + + sb.append(" "); + sb.append(esn); + sb.append(" "); + + if (hasAliasList()) + { + Alias alias = getAliasList().getESNAlias(esn); + + if (alias != null) + { + sb.append(alias.getName()); + } + } + + sb.append("\n"); + } + } + + sb.append("\nNeighbor Sites\n"); + + if (mNeighborIDs.isEmpty()) + { + sb.append(" None\n"); + } + else + { + Iterator it = mNeighborIDs.iterator(); + + while (it.hasNext()) + { + String neighbor = it.next(); + + sb.append(" "); + sb.append(neighbor); + sb.append(" "); + + if (hasAliasList()) + { + Alias alias = getAliasList().getSiteID(String.valueOf(neighbor)); + + if (alias != null) + { + sb.append(alias.getName()); + } + } + + sb.append("\n"); + } + } + + return sb.toString(); } - /** - * Call Detect - current channel messages indicate a call on another channel - */ - private void processCallDetectMessage( LTRNetMessage message ) - { - if( !mActiveCalls.containsKey( message.getChannel() ) || - !mActiveCalls.get( message.getChannel() ).contentEquals( message.getTalkgroupID() ) ) - { - mActiveCalls.put( message.getChannel(), message.getTalkgroupID() ); - - int channel = message.getChannel(); + /** + * Call Detect - current channel messages indicate a call on another channel + */ + private void processCallDetectMessage(LTRNetMessage message) + { + if (!mActiveCalls.containsKey(message.getChannel()) || + !mActiveCalls.get(message.getChannel()).contentEquals(message.getTalkgroupID())) + { + mActiveCalls.put(message.getChannel(), message.getTalkgroupID()); - long frequency = 0; - - if( mTransmitFrequencies.containsKey( channel ) ) - { - frequency = mTransmitFrequencies.get( channel ); - } + int channel = message.getChannel(); - broadcast( new LTRCallEvent.Builder( DecoderType.LTR_NET, CallEventType.CALL_DETECT ) - .aliasList( getAliasList() ) - .channel( String.valueOf( message.getChannel() ) ) - .frequency( frequency ) - .to( message.getTalkgroupID() ) - .build() ); - } - - broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.IDLE ) ); - } - - /** - * Indicates if the talkgroup is different than the talkgroup specified in - * the current call event - */ - private boolean isDifferentTalkgroup( String talkgroup ) - { - return talkgroup != null && - mCurrentCallEvent != null && - mCurrentCallEvent.getToID() != null && - !mCurrentCallEvent.getToID().contentEquals( talkgroup ); - } - - private void processCallMessage( LTRNetMessage message ) - { - int group = message.getGroup(); + long frequency = 0; + + if (mTransmitFrequencies.containsKey(channel)) + { + frequency = mTransmitFrequencies.get(channel); + } + + broadcast(new LTRCallEvent.Builder(DecoderType.LTR_NET, CallEventType.CALL_DETECT) + .aliasList(getAliasList()) + .channel(String.valueOf(message.getChannel())) + .frequency(frequency) + .to(message.getTalkgroupID()) + .build()); + } + + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.IDLE)); + } + + /** + * Indicates if the talkgroup is different than the talkgroup specified in + * the current call event + */ + private boolean isDifferentTalkgroup(String talkgroup) + { + return talkgroup != null && + mCurrentCallEvent != null && + mCurrentCallEvent.getToID() != null && + !mCurrentCallEvent.getToID().contentEquals(talkgroup); + } + + private boolean isValidTalkgroup(String talkgroup) + { + return talkgroup != null && !talkgroup.matches("0-00-000"); + } + + private void processCallMessage(LTRNetMessage message) + { + int group = message.getGroup(); /* Process call registration */ - if( group == 253 ) - { - broadcast( new DecoderStateEvent( this, Event.START, State.DATA ) ); - mDescription = "REGISTER"; - broadcast( ChangedAttribute.DESCRIPTION ); - } - /* Process call */ - else - { - String talkgroup = message.getTalkgroupID(); + if (group == 253) + { + broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); + mDescription = "REGISTER"; + broadcast(ChangedAttribute.DESCRIPTION); + } + /* Process call */ + else + { + String talkgroup = message.getTalkgroupID(); + + if (isValidTalkgroup(talkgroup)) + { + final LTRCallEvent current = getCurrentLTRCallEvent(); - final LTRCallEvent current = getCurrentLTRCallEvent(); - /* If this is a new call or the talkgroup is different from the current * call, create a new call event */ - if( current == null || isDifferentTalkgroup( talkgroup ) ) - { + if (current == null || isDifferentTalkgroup(talkgroup)) + { /* Invalidate the current call */ - if( current != null ) - { - current.setValid( false ); - broadcast( current ); - } - - mTalkgroup = message.getTalkgroupID(); - broadcast( ChangedAttribute.TO_TALKGROUP ); + if (current != null) + { + current.setValid(false); + broadcast(current); + } + + mTalkgroup = message.getTalkgroupID(); + broadcast(ChangedAttribute.TO_TALKGROUP); /* A talkgroup must be seen at least once before it will be added * to the mTalkgroups list that is used in the activity summary, * so that we don't pollute the summary with one-off error talkgroups */ - if( mTalkgroupsFirstHeard.contains( mTalkgroup ) ) - { - mTalkgroups.add( mTalkgroup ); - } - else - { - mTalkgroupsFirstHeard.add( mTalkgroup ); - } - - mTalkgroupAlias = message.getTalkgroupIDAlias(); - broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); + if (mTalkgroupsFirstHeard.contains(mTalkgroup)) + { + mTalkgroups.add(mTalkgroup); + } + else + { + mTalkgroupsFirstHeard.add(mTalkgroup); + } - CallEvent callEvent = new LTRCallEvent.Builder( DecoderType.LTR_NET, CallEventType.CALL ) - .aliasList( getAliasList() ) - .channel( String.valueOf( message.getChannel() ) ) - .frequency( mFrequency ) - .to( message.getTalkgroupID() ) - .build(); - - broadcast( callEvent ); - - mCurrentCallEvent = callEvent; - - broadcast( new Metadata( MetadataType.TO, mTalkgroup, mTalkgroupAlias, true ) ); + mTalkgroupAlias = message.getTalkgroupIDAlias(); + broadcast(ChangedAttribute.TO_TALKGROUP_ALIAS); - broadcast( new DecoderStateEvent( this, Event.START, State.CALL ) ); - } + CallEvent callEvent = new LTRCallEvent.Builder(DecoderType.LTR_NET, CallEventType.CALL) + .aliasList(getAliasList()) + .channel(String.valueOf(message.getChannel())) + .frequency(mFrequency) + .to(message.getTalkgroupID()) + .build(); - broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CALL ) ); - } - } - - private void processCallEndMessage( LTRNetMessage message ) - { - if( mCurrentCallEvent != null && - mCurrentCallEvent.getCallEventType() == CallEventType.CALL ) - { - mCurrentCallEvent.setEnd( System.currentTimeMillis() ); - broadcast( mCurrentCallEvent ); - } + broadcast(callEvent); - mCurrentCallEvent = null; - - broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) ); - } + mCurrentCallEvent = callEvent; - /** - * Performs a full reset - */ - public void reset() - { - mChannelNumber = 0; - - mActiveCalls.clear(); - mESNs.clear(); - mNeighborIDs.clear(); - mReceiveFrequencies.clear(); - mSiteIDs.clear(); - mTalkgroups.clear(); - mTransmitFrequencies.clear(); - mUniqueIDs.clear(); - - resetState(); - } + broadcast(new Metadata(MetadataType.TO, mTalkgroup, mTalkgroupAlias, true)); - /** - * Resets the decoder state after a call or other decode event - */ - private void resetState() - { - if( mCurrentCallEvent != null && mCurrentCallEvent - .getCallEventType() == CallEventType.CALL ) - { - mCurrentCallEvent.end(); - broadcast( mCurrentCallEvent ); - } - - mCurrentCallEvent = null; - - mTalkgroup = null; - broadcast( ChangedAttribute.TO_TALKGROUP ); + broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); + } - mTalkgroupAlias = null; - broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); + } + } + } - mDescription = null; - broadcast( ChangedAttribute.DESCRIPTION ); - } + private void processCallEndMessage(LTRNetMessage message) + { + if (mCurrentCallEvent != null && + mCurrentCallEvent.getCallEventType() == CallEventType.CALL) + { + mCurrentCallEvent.setEnd(System.currentTimeMillis()); + broadcast(mCurrentCallEvent); + } - public String getToTalkgroup() - { - return mTalkgroup; - } - - public Alias getToTalkgroupAlias() - { - return mTalkgroupAlias; - } - - public String getDescription() - { - return mDescription; - } - - public boolean hasChannelNumber() - { - return mChannelNumber != 0; - } - - public int getChannelNumber() - { - return mChannelNumber; - } + mCurrentCallEvent = null; - @Override - public void init() - { + broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); + } + + /** + * Performs a full reset + */ + public void reset() + { + mChannelNumber = 0; + + mActiveCalls.clear(); + mESNs.clear(); + mNeighborIDs.clear(); + mReceiveFrequencies.clear(); + mSiteIDs.clear(); + mTalkgroups.clear(); + mTransmitFrequencies.clear(); + mUniqueIDs.clear(); + + resetState(); + } + + /** + * Resets the decoder state after a call or other decode event + */ + private void resetState() + { + if (mCurrentCallEvent != null && mCurrentCallEvent + .getCallEventType() == CallEventType.CALL) + { + mCurrentCallEvent.end(); + broadcast(mCurrentCallEvent); + } + + mCurrentCallEvent = null; + + mTalkgroup = null; + broadcast(ChangedAttribute.TO_TALKGROUP); + + mTalkgroupAlias = null; + broadcast(ChangedAttribute.TO_TALKGROUP_ALIAS); + + mDescription = null; + broadcast(ChangedAttribute.DESCRIPTION); + } + + public String getToTalkgroup() + { + return mTalkgroup; + } + + public Alias getToTalkgroupAlias() + { + return mTalkgroupAlias; + } + + public String getDescription() + { + return mDescription; + } + + public boolean hasChannelNumber() + { + return mChannelNumber != 0; + } + + public int getChannelNumber() + { + return mChannelNumber; + } + + @Override + public void init() + { /* No initialization steps required */ - } - - @Override - public void receiveDecoderStateEvent( DecoderStateEvent event ) - { - switch( event.getEvent() ) - { - case RESET: - resetState(); - break; - case SOURCE_FREQUENCY: - mFrequency = event.getFrequency(); - break; - default: - break; - } - } + } - @Override - public void start( ScheduledExecutorService executor ) - { - } + @Override + public void receiveDecoderStateEvent(DecoderStateEvent event) + { + switch (event.getEvent()) + { + case RESET: + resetState(); + break; + case SOURCE_FREQUENCY: + mFrequency = event.getFrequency(); + break; + default: + break; + } + } - @Override - public void stop() - { - } + @Override + public void start(ScheduledExecutorService executor) + { + } + + @Override + public void stop() + { + } } diff --git a/src/module/decode/p25/audio/P25AudioModule.java b/src/module/decode/p25/audio/P25AudioModule.java index 8245b981..fbc28f00 100644 --- a/src/module/decode/p25/audio/P25AudioModule.java +++ b/src/module/decode/p25/audio/P25AudioModule.java @@ -25,244 +25,249 @@ import controller.channel.ChannelEvent; import controller.channel.IChannelEventListener; import dsp.gain.NonClippingGain; -public class P25AudioModule extends Module implements Listener, - IAudioPacketProvider, IMessageListener, IChannelEventListener, - IMetadataListener, ISquelchStateListener +public class P25AudioModule extends Module implements Listener, + IAudioPacketProvider, IMessageListener, IChannelEventListener, + IMetadataListener, ISquelchStateListener { - private final static Logger mLog = LoggerFactory.getLogger( P25AudioModule.class ); + private final static Logger mLog = LoggerFactory.getLogger(P25AudioModule.class); - private static final String IMBE_CODEC = "IMBE"; + private static final String IMBE_CODEC = "IMBE"; - /* Provides a unique identifier for this audio module instance to use as a - * source identifier for all audio packets */ - private static int UNIQUE_ID = 0; - private int mSourceID; - private static boolean mLibraryLoadStatusLogged = false; - private boolean mCanConvertAudio = false; - private AudioConverter mAudioConverter; - private Listener mAudioPacketListener; - private AudioMetadata mAudioMetadata; - private ChannelEventListener mChannelEventListener = new ChannelEventListener(); - private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); - private NonClippingGain mGain = new NonClippingGain( 5.0f, 0.95f ); + /* Provides a unique identifier for this audio module instance to use as a + * source identifier for all audio packets */ + private static int UNIQUE_ID = 0; + private int mSourceID; + private static boolean mLibraryLoadStatusLogged = false; + private boolean mCanConvertAudio = false; + private AudioConverter mAudioConverter; + private Listener mAudioPacketListener; + private AudioMetadata mAudioMetadata; + private ChannelEventListener mChannelEventListener = new ChannelEventListener(); + private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); + private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f); - public P25AudioModule( boolean record ) - { - mSourceID = ++UNIQUE_ID; - mAudioMetadata = new AudioMetadata( mSourceID, record ); - - loadConverter(); - } - - @Override - public Listener getMessageListener() - { - return this; - } + public P25AudioModule(boolean record) + { + mSourceID = ++UNIQUE_ID; + mAudioMetadata = new AudioMetadata(mSourceID, record); - @Override - public Listener getSquelchStateListener() - { - return mSquelchStateListener; - } + loadConverter(); + } - @Override - public void dispose() - { - mAudioConverter = null; - } + @Override + public Listener getMessageListener() + { + return this; + } - @Override - public void reset() - { - mAudioMetadata.reset(); - } + @Override + public Listener getSquelchStateListener() + { + return mSquelchStateListener; + } - @Override - public void start( ScheduledExecutorService executor ) - { - - } + @Override + public void dispose() + { + mAudioConverter = null; + } - @Override - public void stop() - { - - } + @Override + public void reset() + { + mAudioMetadata.reset(); + } + + @Override + public void start(ScheduledExecutorService executor) + { + + } + + @Override + public void stop() + { + + } + + /** + * Primary processing method for p25 imbe audio frame messages. Each LDU + * audio message contains 9 imbe voice frames. Each frame is converted to + * audio when the JMBE library is loaded, processed by auto-gain, and + * broadcast to the audio packet listener. + */ + public void receive(Message message) + { + if (mCanConvertAudio && mAudioPacketListener != null) + { + if (message instanceof LDUMessage) + { + LDUMessage ldu = (LDUMessage) message; - /** - * Primary processing method for p25 imbe audio frame messages. Each LDU - * audio message contains 9 imbe voice frames. Each frame is converted to - * audio when the JMBE library is loaded, processed by auto-gain, and - * broadcast to the audio packet listener. - */ - public void receive( Message message ) - { - if( mCanConvertAudio && mAudioPacketListener != null ) - { - if( message instanceof LDUMessage ) - { - LDUMessage ldu = (LDUMessage)message; - /* Only process unencrypted audio frames */ - if( !( ldu.isValid() && ldu.isEncrypted() ) ) - { - for( byte[] frame: ldu.getIMBEFrames() ) - { - float[] audio = mAudioConverter.decode( frame ); + if (!(ldu.isValid() && ldu.isEncrypted())) + { + for (byte[] frame : ldu.getIMBEFrames()) + { + float[] audio = mAudioConverter.decode(frame); - audio = mGain.apply( audio ); + audio = mGain.apply(audio); - mAudioPacketListener.receive( - new AudioPacket( audio, mAudioMetadata.copyOf() ) ); - } - } - } - } - } + mAudioPacketListener.receive( + new AudioPacket(audio, mAudioMetadata.copyOf())); + } + } + } + } + } - /** - * Loads audio frame processing chain. Constructs an imbe targetdataline - * to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion - * stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper. - */ - private void loadConverter() - { - AudioConversionLibrary library = null; - - try - { - @SuppressWarnings( "rawtypes" ) - Class temp = Class.forName( "jmbe.JMBEAudioLibrary" ); - - library = (AudioConversionLibrary)temp.newInstance(); - - if( ( library.getMajorVersion() == 0 && - library.getMinorVersion() >= 3 && - library.getBuildVersion() >= 1 ) || - library.getMajorVersion() >= 1 ) - { - mAudioConverter = library.getAudioConverter( IMBE_CODEC, - AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO ); - - if( mAudioConverter != null ) - { - mCanConvertAudio = true; - - if( !mLibraryLoadStatusLogged ) - { - mLog.info( "JMBE audio conversion library successfully loaded" - + " - P25 audio will be available" ); - - mLibraryLoadStatusLogged = true; - } - } - else - { - if( !mLibraryLoadStatusLogged ) - { - mLog.info( "JMBE audio conversion library NOT FOUND" ); - mLibraryLoadStatusLogged = true; - } - } - } - else - { - mLog.warn( "JMBE library version 0.3.1 or higher is required - found: " + library.getVersion() ); - } - } - catch ( ClassNotFoundException e1 ) - { - if( !mLibraryLoadStatusLogged ) - { - mLog.error( "Couldn't find/load JMBE audio conversion library" ); - mLibraryLoadStatusLogged = true; - } - } - catch ( InstantiationException e1 ) - { - if( !mLibraryLoadStatusLogged ) - { - mLog.error( "Couldn't instantiate JMBE audio conversion library class" ); - mLibraryLoadStatusLogged = true; - } - } - catch ( IllegalAccessException e1 ) - { - if( !mLibraryLoadStatusLogged ) - { - mLog.error( "Couldn't load JMBE audio conversion library due to " - + "security restrictions" ); - mLibraryLoadStatusLogged = true; - } - } - } + /** + * Loads audio frame processing chain. Constructs an imbe targetdataline + * to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion + * stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper. + */ + private void loadConverter() + { + AudioConversionLibrary library = null; - @Override - public void setAudioPacketListener( Listener listener ) - { - mAudioPacketListener = listener; - } + try + { + @SuppressWarnings("rawtypes") + Class temp = Class.forName("jmbe.JMBEAudioLibrary"); - @Override - public void removeAudioPacketListener() - { - mAudioPacketListener = null; - } - - @Override - public Listener getMetadataListener() - { - return mAudioMetadata; - } + library = (AudioConversionLibrary) temp.newInstance(); - @Override - public Listener getChannelEventListener() - { - return mChannelEventListener; - } + if ((library.getMajorVersion() == 0 && + library.getMinorVersion() >= 3 && + library.getBuildVersion() >= 3) || + library.getMajorVersion() >= 1) + { +// mAudioConverter = library.getAudioConverter( IMBE_CODEC, +// AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO ); + mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); - /** - * Wrapper for channel event listener. Responds to channel state reset - * events to remove/cleanup current audio metadata - */ - public class ChannelEventListener implements Listener - { - @Override - public void receive( ChannelEvent event ) - { - switch( event.getEvent() ) - { - case NOTIFICATION_STATE_RESET: - mAudioMetadata.reset(); - break; - case NOTIFICATION_SELECTION_CHANGE: - mAudioMetadata.setSelected( event.getChannel().isSelected() ); - break; - default: - break; - } - } - } - - /** - * Wrapper for squelch state listener. Internally, the P25 audio module - * doesn't have a squelch state. If there are IMBE audio frames, we have - * audio. We use this listener to signal to the recorder manager that the - * channel state is resetting and it should end the recording. - */ - public class SquelchStateListener implements Listener - { - @Override - public void receive( SquelchState state ) - { - if( state == SquelchState.SQUELCH && mAudioPacketListener != null ) - { - mAudioPacketListener.receive( new AudioPacket( AudioPacket.Type.END, - mAudioMetadata.copyOf() ) ); + if (mAudioConverter != null) + { + mCanConvertAudio = true; - mAudioMetadata.reset(); - } - } - } + if (!mLibraryLoadStatusLogged) + { + StringBuilder sb = new StringBuilder(); + sb.append("JMBE audio conversion library ["); + sb.append(library.getVersion()); + sb.append("] successfully loaded - P25 audio will be available"); + + mLog.info(sb.toString()); + + mLibraryLoadStatusLogged = true; + } + } + else + { + if (!mLibraryLoadStatusLogged) + { + mLog.info("JMBE audio conversion library NOT FOUND"); + mLibraryLoadStatusLogged = true; + } + } + } + else + { + mLog.warn("JMBE library version 0.3.1 or higher is required - found: " + library.getVersion()); + } + } + catch (ClassNotFoundException e1) + { + if (!mLibraryLoadStatusLogged) + { + mLog.error("Couldn't find/load JMBE audio conversion library"); + mLibraryLoadStatusLogged = true; + } + } + catch (InstantiationException e1) + { + if (!mLibraryLoadStatusLogged) + { + mLog.error("Couldn't instantiate JMBE audio conversion library class"); + mLibraryLoadStatusLogged = true; + } + } + catch (IllegalAccessException e1) + { + if (!mLibraryLoadStatusLogged) + { + mLog.error("Couldn't load JMBE audio conversion library due to " + + "security restrictions"); + mLibraryLoadStatusLogged = true; + } + } + } + + @Override + public void setAudioPacketListener(Listener listener) + { + mAudioPacketListener = listener; + } + + @Override + public void removeAudioPacketListener() + { + mAudioPacketListener = null; + } + + @Override + public Listener getMetadataListener() + { + return mAudioMetadata; + } + + @Override + public Listener getChannelEventListener() + { + return mChannelEventListener; + } + + /** + * Wrapper for channel event listener. Responds to channel state reset + * events to remove/cleanup current audio metadata + */ + public class ChannelEventListener implements Listener + { + @Override + public void receive(ChannelEvent event) + { + switch (event.getEvent()) + { + case NOTIFICATION_STATE_RESET: + mAudioMetadata.reset(); + break; + case NOTIFICATION_SELECTION_CHANGE: + mAudioMetadata.setSelected(event.getChannel().isSelected()); + break; + default: + break; + } + } + } + + /** + * Wrapper for squelch state listener. Internally, the P25 audio module + * doesn't have a squelch state. If there are IMBE audio frames, we have + * audio. We use this listener to signal to the recorder manager that the + * channel state is resetting and it should end the recording. + */ + public class SquelchStateListener implements Listener + { + @Override + public void receive(SquelchState state) + { + if (state == SquelchState.SQUELCH && mAudioPacketListener != null) + { + mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, + mAudioMetadata.copyOf())); + + mAudioMetadata.reset(); + } + } + } } diff --git a/src/record/RecorderManager.java b/src/record/RecorderManager.java index 4cc30ab2..cd7c526f 100644 --- a/src/record/RecorderManager.java +++ b/src/record/RecorderManager.java @@ -37,7 +37,7 @@ public class RecorderManager implements Listener { private static final Logger mLog = LoggerFactory.getLogger( RecorderManager.class ); - public static final int AUDIO_SAMPLE_RATE = 48000; + public static final int AUDIO_SAMPLE_RATE = 8000; private Map mRecorders = new HashMap<>(); diff --git a/src/sample/real/RealToRealBufferAssembler.java b/src/sample/real/RealToRealBufferAssembler.java index 0efe048f..3a476af7 100644 --- a/src/sample/real/RealToRealBufferAssembler.java +++ b/src/sample/real/RealToRealBufferAssembler.java @@ -1,19 +1,19 @@ /******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see + * SDR Trunk + * Copyright (C) 2015 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see ******************************************************************************/ package sample.real; @@ -24,51 +24,49 @@ import sample.Listener; */ public class RealToRealBufferAssembler implements RealSampleListener { - private int mBufferSize; - private float[] mBuffer; - private int mBufferPointer; - - private Listener mListener; - - public RealToRealBufferAssembler( int bufferSize ) - { - mBufferSize = bufferSize; - mBuffer = new float[ mBufferSize ]; - } - - public void dispose() - { - mListener = null; - mBuffer = null; - } - - public void reset() - { - mBuffer = new float[ mBufferSize ]; - mBufferPointer = 0; - } - - @Override - public void receive( float sample ) - { - if( mBufferPointer >= mBufferSize ) - { - if( mListener != null ) - { - mListener.receive( new RealBuffer( mBuffer ) ); - } + private int mBufferSize; + private float[] mBuffer; + private int mBufferPointer; - mBuffer = new float[ mBufferSize ]; - mBufferPointer = 0; - } + private Listener mListener; - mBuffer[ mBufferPointer ] = sample; + public RealToRealBufferAssembler(int bufferSize) + { + mBufferSize = bufferSize; + mBuffer = new float[mBufferSize]; + } - mBufferPointer++; - } + public void dispose() + { + mListener = null; + mBuffer = null; + } - public void setListener( Listener listener ) - { - mListener = listener; - } + public void reset() + { + mBuffer = new float[mBufferSize]; + mBufferPointer = 0; + } + + @Override + public void receive(float sample) + { + mBuffer[mBufferPointer++] = sample; + + if (mBufferPointer >= mBufferSize) + { + if (mListener != null) + { + mListener.receive(new RealBuffer(mBuffer)); + } + + mBuffer = new float[mBufferSize]; + mBufferPointer = 0; + } + } + + public void setListener(Listener listener) + { + mListener = listener; + } } diff --git a/src/source/tuner/rtl/RTL2832TunerController.java b/src/source/tuner/rtl/RTL2832TunerController.java index 140b6058..e102d4d7 100644 --- a/src/source/tuner/rtl/RTL2832TunerController.java +++ b/src/source/tuner/rtl/RTL2832TunerController.java @@ -1599,7 +1599,7 @@ public abstract class RTL2832TunerController extends TunerController } catch( Exception e ) { - mLog.error( "error duing rtl2832 buffer dispatcher run", e ); + mLog.error( "error during rtl2832 buffer dispatcher run", e ); } } }