Skip to content

Commit

Permalink
#35 WIP Updated AM decoder to adjust demodulator gain and implemented…
Browse files Browse the repository at this point in the history
… new Gain and DC removal control that attempts to normalize audio gain toward an objective value.
  • Loading branch information
Dennis Sheirer committed May 2, 2023
1 parent 4147f75 commit 749613c
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 141 deletions.
140 changes: 140 additions & 0 deletions 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/

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;
}
}

This file was deleted.

@@ -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
Expand All @@ -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;
Expand All @@ -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
*/
Expand Down Expand Up @@ -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),
Expand Down
45 changes: 9 additions & 36 deletions src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java
Expand Up @@ -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
Expand All @@ -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));
}

/**
Expand All @@ -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.");
}
}
Expand Up @@ -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;

/**
Expand All @@ -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()
{
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -142,12 +141,6 @@ private void endCallEvent()
resetState();
}

@Override
public DecoderType getDecoderType()
{
return DecoderType.NBFM;
}

@Override
public void start()
{
Expand Down

0 comments on commit 749613c

Please sign in to comment.