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..b89f493d 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,213 @@ 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 + { + 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(); - @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; + + //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); + + mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6); + 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..8285cf84 100644 --- a/src/dsp/filter/design/FilterViewer.java +++ b/src/dsp/filter/design/FilterViewer.java @@ -41,14 +41,14 @@ public class FilterViewer extends Application private float[] getFilter() { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate( 10000 ) + .sampleRate( 48000 ) .gridDensity( 16 ) - .passBandCutoff( 1300 ) + .passBandCutoff( 3000 ) .passBandAmplitude( 1.0 ) .passBandRipple( 0.01 ) - .stopBandStart( 2675 ) + .stopBandStart( 4000 ) .stopBandAmplitude( 0.0 ) - .stopBandRipple( 0.001 ) + .stopBandRipple( 0.027 ) .build(); // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() 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..33212c0b --- /dev/null +++ b/src/dsp/filter/polyphase/PolyphaseFIRDecimatingFilter_RB.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * 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; + + /** + * 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. + */ + public PolyphaseFIRDecimatingFilter_RB(float[] coefficients, int decimationRatio) + { + mDecimationRatio = decimationRatio; + 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); + 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); + 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/p25/audio/P25AudioModule.java b/src/module/decode/p25/audio/P25AudioModule.java index 8245b981..4d062945 100644 --- a/src/module/decode/p25/audio/P25AudioModule.java +++ b/src/module/decode/p25/audio/P25AudioModule.java @@ -25,244 +25,245 @@ 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) + { + 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; + } + } + } + + @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 ); } } }