diff --git a/src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java b/src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java new file mode 100644 index 000000000..c329f3998 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java @@ -0,0 +1,140 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 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 io.github.dsheirer.dsp.gain; + +import org.apache.commons.math3.util.FastMath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Audio gain that normalizes audio amplitude against an objective amplitude value where the audio amplitude values + * normally fall in the range of -1.0 to 1.0. + * + * Removes DC bias on a per audio sample buffer basis. + * + * This control is designed to work on a per-call basis by monitoring the maximum observed amplitude and adjusting the + * gain to normalize the amplitude toward the objective amplitude value. This control is designed to be reset at the + * beginning/end of each call segment so that the max observed amplitude is relevant to the current call segment. + * + * Min and max gains should be sized to constrain the applied value during periods of extended silence at the + * beginning of an audio segment or where a single amplitude spike might erroneously affect gain across the segment. + */ +public class AudioGainAndDcFilter +{ + private static final Logger mLog = LoggerFactory.getLogger(AudioGainAndDcFilter.class); + private float mMinGain; + private float mMaxGain; + private float mCurrentGain; + private float mObjectiveGain; + private float mObjectiveAmplitude; + private float mMaxObservedAmplitude; + private static float MAX_AMPLITUDE = 0.95f; + //Stabilizes gain after 2x buffers of 2048 samples + private static final float GAIN_LOOP_BANDWIDTH = 0.0015f; + + /** + * Constructs an instance + * @param minGain to apply to the incoming sample stream. + * @param maxGain to apply + * @param objectiveAmplitude to achieve by adjusting gain between min and max. + */ + public AudioGainAndDcFilter(float minGain, float maxGain, float objectiveAmplitude) + { + mMinGain = minGain; + mMaxGain = maxGain; + mObjectiveAmplitude = objectiveAmplitude; + reset(); + } + + /** + * Resets this gain control to prepare for the next audio call/segment. + */ + public void reset() + { + mMaxObservedAmplitude = 0.0f; + mObjectiveGain = 1.0f; + mCurrentGain = 1.0f; + } + + /** + * Process a buffer of audio samples and apply gain. + * @param samples to adjust. + * @return amplified audio samples + */ + public float[] process(float[] samples) + { + float currentAmplitude; + float dcAccumulator = 0.0f; + + //Decay the max observed value by 10% each buffer so that an initial spike doesn't carry across all buffers + mMaxObservedAmplitude *= 0.9f; + + for(float sample: samples) + { + dcAccumulator += sample; + currentAmplitude = FastMath.min(Math.abs(sample), 1.2f); + + if(currentAmplitude > mMaxObservedAmplitude) + { + mMaxObservedAmplitude = currentAmplitude; + } + } + + mObjectiveGain = mObjectiveAmplitude / mMaxObservedAmplitude; + + if(mObjectiveGain > mMaxGain) + { + mObjectiveGain = mMaxGain; + } + else if(mObjectiveGain < mMinGain) + { + mObjectiveGain = mMinGain; + } + + float dcOffset = dcAccumulator / samples.length; + float gain = mCurrentGain; + float objective = mObjectiveGain; + float[] processed = new float[samples.length]; + boolean objectiveAchieved = (gain == objective); + float amplified; + + for(int x = 0; x < samples.length; x++) + { + if(!objectiveAchieved) + { + gain += ((objective - gain) * GAIN_LOOP_BANDWIDTH); + if(Math.abs(objective - gain) < 0.00005f) + { + gain = objective; + objectiveAchieved = true; + } + } + + amplified = (samples[x] - dcOffset) * gain; + amplified = FastMath.min(amplified, MAX_AMPLITUDE); + amplified = FastMath.max(amplified, -MAX_AMPLITUDE); + processed[x] = amplified; + } + + mCurrentGain = gain; + mObjectiveGain = objective; + return processed; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/gain/ObjectiveGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/ObjectiveGainControl.java deleted file mode 100644 index 453576db2..000000000 --- a/src/main/java/io/github/dsheirer/dsp/gain/ObjectiveGainControl.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.github.dsheirer.dsp.gain; - -import org.apache.commons.math3.util.FastMath; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ObjectiveGainControl -{ - private static final Logger mLog = LoggerFactory.getLogger(ObjectiveGainControl.class); - private float mMinGain; - private float mMaxGain; - private float mCurrentGain = 1.0f; - private float mObjectiveGain = mCurrentGain; - private float mObjectiveAmplitude; - private float mMaxObservedAmplitude; - - public ObjectiveGainControl(float minGain, float maxGain, float objectiveAmplitude) - { - mMinGain = minGain; - mMaxGain = maxGain; - mObjectiveAmplitude = objectiveAmplitude; - } - - public void reset() - { - mMaxObservedAmplitude = 0.0f; - } - - public float[] process(float[] samples) - { - float currentAmplitude; - - for(float sample: samples) - { - currentAmplitude = FastMath.abs(sample); - - if(currentAmplitude > mMaxObservedAmplitude) - { - mMaxObservedAmplitude = currentAmplitude; - } - } - - mObjectiveGain = mObjectiveAmplitude / mMaxObservedAmplitude; - - if(mObjectiveGain > mMaxGain) - { - mObjectiveGain = mMaxGain; - } - else if(mObjectiveGain < mMinGain) - { - mObjectiveGain = mMinGain; - } - - if(mCurrentGain != mObjectiveGain) - { - mLog.info("Current: " + mCurrentGain + " Objective: " + mObjectiveGain); - float incrementalGainChange = mObjectiveGain / samples.length / 4; //Aim to achieve objective over 4x sample buffers - float gain = mCurrentGain; - - float[] processed = new float[samples.length]; - for(int x = 0; x < samples.length; x++) - { - gain += incrementalGainChange; - - if(gain > mMaxGain) - { - gain = mMaxGain; - } - if(gain < mMinGain) - { - gain = mMinGain; - } - - processed[x] = samples[x] * gain; - } - - if(Math.abs(mObjectiveGain - mCurrentGain) < incrementalGainChange) - { - mLog.info("Objective reached!"); - mCurrentGain = mObjectiveGain; - } - else - { - mCurrentGain = gain; - } - - return processed; - } - else - { - return samples; - } - } -} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java index 150912a31..efdc4b6fd 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +27,8 @@ import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.protocol.Protocol; +import java.util.ArrayList; +import java.util.List; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.HPos; @@ -38,9 +40,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - /** * Editor for talkgroup alias identifiers */ @@ -238,6 +237,10 @@ private TalkgroupDetail getTalkgroupDetail(Protocol protocol, IntegerFormat inte private void loadTalkgroupDetails() { mTalkgroupDetails.clear(); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF), + "Format: 1 - 65535")); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF), + "Format: 1 - FFFF")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535), "Format: 0 - 65535")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), diff --git a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java index ca0adabf4..3239ee917 100644 --- a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java @@ -20,23 +20,22 @@ package io.github.dsheirer.module.decode.am; import io.github.dsheirer.dsp.am.SquelchingAMDemodulator; -import io.github.dsheirer.dsp.gain.AutomaticGainControl; -import io.github.dsheirer.dsp.gain.ObjectiveGainControl; +import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.analog.SquelchingAnalogDecoder; -import io.github.dsheirer.source.wave.RealWaveSource; - -import java.io.File; /** - * Decoder module with integrated squelching AM demodulator + * Analog AM radio decoder module with integrated squelching control */ public class AMDecoder extends SquelchingAnalogDecoder { - private static final float DEMODULATOR_GAIN = 400.0f; //400 seems about right for a strong local signal + private static final float DEMODULATOR_GAIN = 250.0f; private static final float SQUELCH_ALPHA_DECAY = 0.0004f; private static final float SQUELCH_THRESHOLD_DB = -78.0f; - private AutomaticGainControl mAGC = new AutomaticGainControl(); + private static final float MINIMUM_GAIN = 0.5f; + private static final float MAXIMUM_GAIN = 16.0f; + private static final float OBJECTIVE_AUDIO_AMPLITUDE = 0.75f; + private AudioGainAndDcFilter mAGC = new AudioGainAndDcFilter(MINIMUM_GAIN, MAXIMUM_GAIN, OBJECTIVE_AUDIO_AMPLITUDE); /** * Constructs an instance @@ -60,8 +59,8 @@ public DecoderType getDecoderType() @Override protected void broadcast(float[] demodulatedSamples) { - float[] amplified = mAGC.process(demodulatedSamples); - super.broadcast(amplified); + //Apply audio gain and rebroadcast + super.broadcast(mAGC.process(demodulatedSamples)); } /** @@ -73,30 +72,4 @@ protected void notifyCallStart() mAGC.reset(); super.notifyCallStart(); } - - public static void main(String[] args) - { - ObjectiveGainControl agc = new ObjectiveGainControl(1.0f, 4.0f, 0.7f); - - String path = "C:\\Users\\sheirerd\\SDRTrunk\\recordings\\20230430_052528Air_Boston_Center__TO_3.wav"; - File file = new File(path); - - try - { - RealWaveSource source = new RealWaveSource(file); - source.setListener(floats -> agc.process(floats)); - source.open(); - - while(true) - { - source.next(256, true); - } - - } - catch(Exception e) - { -// mLog.error("Error", e); - } - System.out.println("Finished."); - } } diff --git a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java index 60c1f546e..39f49c0f2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java @@ -24,6 +24,7 @@ import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.string.SimpleStringIdentifier; +import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.analog.AnalogDecoderState; /** @@ -47,6 +48,12 @@ public AMDecoderState(String channelName, DecodeConfigAM decodeConfig) mTalkgroupIdentifier = new AMTalkgroup(decodeConfig.getTalkgroup()); } + @Override + public DecoderType getDecoderType() + { + return DecoderType.AM; + } + @Override protected Identifier getChannelNameIdentifier() { diff --git a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java index f4a004a6a..dcfb966ae 100644 --- a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java @@ -25,7 +25,6 @@ import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.message.IMessage; -import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel; import io.github.dsheirer.sample.Listener; @@ -142,12 +141,6 @@ private void endCallEvent() resetState(); } - @Override - public DecoderType getDecoderType() - { - return DecoderType.NBFM; - } - @Override public void start() { diff --git a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java index 4c6867970..e6cb42996 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java @@ -24,6 +24,7 @@ import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.string.SimpleStringIdentifier; +import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.analog.AnalogDecoderState; /** @@ -47,6 +48,12 @@ public NBFMDecoderState(String channelName, DecodeConfigNBFM decodeConfig) mTalkgroupIdentifier = new NBFMTalkgroup(decodeConfig.getTalkgroup()); } + @Override + public DecoderType getDecoderType() + { + return DecoderType.NBFM; + } + @Override protected Identifier getChannelNameIdentifier() {