mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#35 Updates AM & NBFM decoders to use Adaptive Squelch, a power squelch that includes an adaptive noise floor monitor. Implements vectorized magnitude operations for AM & FM decoders. Adds talkgroup assignment and squelch controls to AM channel configuration editor. Implements new AM gain control that attempts to normalize audio gain quickly at beginning of call/transmission and maintains that gain across lifecycle of the call and resets the gain for the next call. (#1540)
Notes on usage: - When using either AM or FM squelch control use either the Heterodyne channelizer or if using the Polyphase channelizer, avoid using trunk tracking decoders like DMR or P25 or set the squelch threshold high enough to avoid changes in noise floor from polyphase channelizer where it can vary between using the one channel or the two-channel combined channel sources. Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 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
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.action.AliasActionManager;
|
||||
import io.github.dsheirer.audio.AbstractAudioModule;
|
||||
import io.github.dsheirer.audio.AudioModule;
|
||||
import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
@@ -36,6 +35,7 @@ import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.am.AMDecoder;
|
||||
import io.github.dsheirer.module.decode.am.AMDecoderState;
|
||||
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
@@ -88,23 +88,20 @@ import io.github.dsheirer.module.decode.passport.PassportMessageFilter;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
|
||||
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
|
||||
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
|
||||
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DecoderFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class);
|
||||
|
||||
private static final double AM_CHANNEL_BANDWIDTH = 3000.0;
|
||||
private static final double FM_CHANNEL_BANDWIDTH = 12500.0;
|
||||
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
|
||||
|
||||
@@ -313,29 +310,23 @@ public class DecoderFactory
|
||||
}
|
||||
|
||||
DecodeConfigNBFM decodeConfigNBFM = (DecodeConfigNBFM)decodeConfig;
|
||||
|
||||
modules.add(new NBFMDecoder(decodeConfigNBFM));
|
||||
modules.add(new NBFMDecoderState(channel.getName(), decodeConfigNBFM));
|
||||
modules.add(new AudioModule(aliasList));
|
||||
}
|
||||
|
||||
private static void processAM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
|
||||
modules.add(new AMDecoder(decodeConfig));
|
||||
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName()));
|
||||
|
||||
AudioModule audioModuleAM = new AudioModule(aliasList);
|
||||
modules.add(audioModuleAM);
|
||||
|
||||
//Check if the user wants all audio recorded ..
|
||||
if(((DecodeConfigAM)decodeConfig).getRecordAudio())
|
||||
if(!(decodeConfig instanceof DecodeConfigAM))
|
||||
{
|
||||
audioModuleAM.setRecordAudio(true);
|
||||
throw new IllegalArgumentException("Can't create AM decoder - unrecognized decode config type: " +
|
||||
(decodeConfig != null ? decodeConfig.getClass() : "null/empty"));
|
||||
}
|
||||
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new AMDemodulatorModule(AM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
DecodeConfigAM decodeConfigAM = (DecodeConfigAM) decodeConfig;
|
||||
modules.add(new AMDecoder(decodeConfigAM));
|
||||
modules.add(new AMDecoderState(channel.getName(), decodeConfigAM));
|
||||
modules.add(new AudioModule(aliasList));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -506,8 +497,6 @@ public class DecoderFactory
|
||||
|
||||
public static DecodeConfiguration getDecodeConfiguration(DecoderType decoder)
|
||||
{
|
||||
DecodeConfiguration retVal;
|
||||
|
||||
switch(decoder)
|
||||
{
|
||||
case AM:
|
||||
@@ -545,7 +534,10 @@ public class DecoderFactory
|
||||
case AM:
|
||||
DecodeConfigAM copyAM = new DecodeConfigAM();
|
||||
DecodeConfigAM origAM = (DecodeConfigAM)config;
|
||||
copyAM.setRecordAudio(origAM.getRecordAudio());
|
||||
copyAM.setBandwidth(origAM.getBandwidth());
|
||||
copyAM.setTalkgroup(origAM.getTalkgroup());
|
||||
copyAM.setSquelchThreshold(origAM.getSquelchThreshold());
|
||||
copyAM.setSquelchAutoTrack(origAM.isSquelchAutoTrack());
|
||||
return copyAM;
|
||||
case DMR:
|
||||
return new DecodeConfigDMR();
|
||||
@@ -571,6 +563,9 @@ public class DecoderFactory
|
||||
DecodeConfigNBFM origNBFM = (DecodeConfigNBFM)config;
|
||||
DecodeConfigNBFM copyNBFM = new DecodeConfigNBFM();
|
||||
copyNBFM.setBandwidth(origNBFM.getBandwidth());
|
||||
copyNBFM.setSquelchThreshold(origNBFM.getSquelchThreshold());
|
||||
copyNBFM.setSquelchAutoTrack(origNBFM.isSquelchAutoTrack());
|
||||
copyNBFM.setTalkgroup(origNBFM.getTalkgroup());
|
||||
return copyNBFM;
|
||||
case P25_PHASE1:
|
||||
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config;
|
||||
|
||||
@@ -1,52 +1,75 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.am;
|
||||
|
||||
import io.github.dsheirer.dsp.am.SquelchingAMDemodulator;
|
||||
import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.analog.SquelchingAnalogDecoder;
|
||||
|
||||
public class AMDecoder extends PrimaryDecoder
|
||||
/**
|
||||
* Analog AM radio decoder module with integrated squelching control
|
||||
*/
|
||||
public class AMDecoder extends SquelchingAnalogDecoder
|
||||
{
|
||||
public AMDecoder( DecodeConfiguration config )
|
||||
{
|
||||
super( config );
|
||||
}
|
||||
private static final float DEMODULATOR_GAIN = 250.0f;
|
||||
private static final float SQUELCH_ALPHA_DECAY = 0.0004f;
|
||||
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);
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param config for the AM channel.
|
||||
*/
|
||||
public AMDecoder(DecodeConfigAM config)
|
||||
{
|
||||
return DecoderType.AM;
|
||||
super(config, new SquelchingAMDemodulator(DEMODULATOR_GAIN, SQUELCH_ALPHA_DECAY,
|
||||
config.getSquelchThreshold(), config.isSquelchAutoTrack()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.AM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
/**
|
||||
* Overrides the broadcast of the final demodulated audio samples so that we can apply AGC
|
||||
* @param demodulatedSamples to broadcast
|
||||
*/
|
||||
@Override
|
||||
protected void broadcast(float[] demodulatedSamples)
|
||||
{
|
||||
//Apply audio gain and rebroadcast
|
||||
super.broadcast(mAGC.process(demodulatedSamples));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the notify call start so that we can reset the AGC
|
||||
*/
|
||||
@Override
|
||||
protected void notifyCallStart()
|
||||
{
|
||||
mAGC.reset();
|
||||
super.notifyCallStart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.am;
|
||||
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
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;
|
||||
|
||||
/**
|
||||
* AM decoder state
|
||||
*/
|
||||
public class AMDecoderState extends AnalogDecoderState
|
||||
{
|
||||
private String mChannelName;
|
||||
private Identifier mChannelNameIdentifier;
|
||||
private Identifier mTalkgroupIdentifier;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param channelName to use for this channel
|
||||
* @param decodeConfig with talkgroup identifier
|
||||
*/
|
||||
public AMDecoderState(String channelName, DecodeConfigAM decodeConfig)
|
||||
{
|
||||
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "AM CHANNEL";
|
||||
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.CONFIGURATION, Form.CHANNEL_NAME, Role.ANY);
|
||||
mTalkgroupIdentifier = new AMTalkgroup(decodeConfig.getTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.AM;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Identifier getChannelNameIdentifier()
|
||||
{
|
||||
return mChannelNameIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Identifier getTalkgroupIdentifier()
|
||||
{
|
||||
return mTalkgroupIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Activity Summary\n");
|
||||
sb.append("\tDecoder: AM");
|
||||
sb.append("\n\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.am;
|
||||
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* AM talkgroup. Note: this is a logical identifier that is assignable from a user-specified
|
||||
* configuration so that AM audio events are compatible with other features of the sdrtrunk system.
|
||||
*/
|
||||
public class AMTalkgroup extends TalkgroupIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param value 1-65,535
|
||||
*/
|
||||
public AMTalkgroup(int value)
|
||||
{
|
||||
super(value, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.AM;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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
|
||||
@@ -15,33 +14,35 @@
|
||||
*
|
||||
* 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.module.decode.am;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
||||
|
||||
public class DecodeConfigAM extends DecodeConfiguration
|
||||
/**
|
||||
* AM decoder configuration
|
||||
*/
|
||||
public class DecodeConfigAM extends DecodeConfigAnalog
|
||||
{
|
||||
private boolean mRecordAudio = false;
|
||||
|
||||
public DecodeConfigAM()
|
||||
{
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "recordAudio")
|
||||
public boolean getRecordAudio()
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return mRecordAudio;
|
||||
return DecoderType.AM;
|
||||
}
|
||||
|
||||
public void setRecordAudio(boolean recordAudio)
|
||||
@Override
|
||||
protected Bandwidth getDefaultBandwidth()
|
||||
{
|
||||
mRecordAudio = recordAudio;
|
||||
return Bandwidth.BW_15_0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,13 +52,20 @@ public class DecodeConfigAM extends DecodeConfiguration
|
||||
@Override
|
||||
public ChannelSpecification getChannelSpecification()
|
||||
{
|
||||
return new ChannelSpecification(25000.0,
|
||||
3000, 3000.0, 5000.0);
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.AM;
|
||||
switch(getBandwidth())
|
||||
{
|
||||
case BW_3_0:
|
||||
return new ChannelSpecification(25000.0, 3000, 1500.0, 1700.0);
|
||||
case BW_5_0:
|
||||
return new ChannelSpecification(25000.0, 5000, 2500.0, 2700.0);
|
||||
case BW_8_33:
|
||||
return new ChannelSpecification(25000.0, 10000, 5000.0, 7000.0);
|
||||
case BW_15_0:
|
||||
return new ChannelSpecification(25000.0, 15000, 7500.0, 9500.0);
|
||||
case BW_25_0:
|
||||
return new ChannelSpecification(25000.0, 25000, 12500.0, 14500.0);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized AM bandwidth value: " + getBandwidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.analog;
|
||||
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract analog decoder channel state - provides the minimum channel state functionality
|
||||
*/
|
||||
public abstract class AnalogDecoderState extends DecoderState implements ISourceEventListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class);
|
||||
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
|
||||
private DecodeEvent mDecodeEvent;
|
||||
private IChannelDescriptor mChannelDescriptor = null;
|
||||
|
||||
public AnalogDecoderState()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel name identifier provided by the subclass implementation.
|
||||
*/
|
||||
protected abstract Identifier getChannelNameIdentifier();
|
||||
|
||||
protected abstract Identifier getTalkgroupIdentifier();
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case REQUEST_RESET ->
|
||||
{
|
||||
getIdentifierCollection().update(getChannelNameIdentifier());
|
||||
}
|
||||
case START ->
|
||||
{
|
||||
if(event.getState() == State.CALL)
|
||||
{
|
||||
startCallEvent();
|
||||
}
|
||||
}
|
||||
case END ->
|
||||
{
|
||||
if(event.getState() == State.CALL)
|
||||
{
|
||||
endCallEvent();
|
||||
}
|
||||
}
|
||||
case CONTINUATION ->
|
||||
{
|
||||
if(event.getState() == State.CALL)
|
||||
{
|
||||
continueCallEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
endCallEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/starts a decode call evnet.
|
||||
*/
|
||||
private void startCallEvent()
|
||||
{
|
||||
getIdentifierCollection().update(getTalkgroupIdentifier());
|
||||
|
||||
if(mDecodeEvent == null)
|
||||
{
|
||||
mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis())
|
||||
.channel(mChannelDescriptor)
|
||||
.eventDescription("CALL")
|
||||
.details(getDecoderType().name())
|
||||
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
|
||||
.build();
|
||||
|
||||
broadcast(mDecodeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues (or starts) the call decode event and updates the current timestamp
|
||||
*/
|
||||
private void continueCallEvent()
|
||||
{
|
||||
if(mDecodeEvent == null)
|
||||
{
|
||||
startCallEvent();
|
||||
}
|
||||
|
||||
mDecodeEvent.update(System.currentTimeMillis());
|
||||
broadcast(mDecodeEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the call decode event
|
||||
*/
|
||||
private void endCallEvent()
|
||||
{
|
||||
if(mDecodeEvent != null)
|
||||
{
|
||||
mDecodeEvent.end(System.currentTimeMillis());
|
||||
broadcast(mDecodeEvent);
|
||||
mDecodeEvent = null;
|
||||
}
|
||||
|
||||
//Remove any user identifiers from the identifier collection
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
getIdentifierCollection().update(getChannelNameIdentifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {}
|
||||
@Override
|
||||
public void init() {}
|
||||
@Override
|
||||
public void receive(IMessage t) {}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceEventListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors source events to capture the channel frequency for use in decode events.
|
||||
*/
|
||||
private class SourceEventListener implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
|
||||
{
|
||||
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.analog;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.dsp.squelch.ISquelchConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Decoder configuration for an squelching analog channel
|
||||
*/
|
||||
public abstract class DecodeConfigAnalog extends DecodeConfiguration implements ISquelchConfiguration
|
||||
{
|
||||
private Bandwidth mBandwidth;
|
||||
private int mTalkgroup = 1;
|
||||
private int mSquelchThreshold = -78;
|
||||
private boolean mSquelchAutoTrack = true;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DecodeConfigAnalog()
|
||||
{
|
||||
mBandwidth = getDefaultBandwidth();
|
||||
}
|
||||
|
||||
protected abstract Bandwidth getDefaultBandwidth();
|
||||
|
||||
/**
|
||||
* Channel bandwidth
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "bandwidth")
|
||||
public Bandwidth getBandwidth()
|
||||
{
|
||||
return mBandwidth;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public ChannelSpecification getChannelSpecification()
|
||||
{
|
||||
if(mBandwidth == Bandwidth.BW_12_5)
|
||||
{
|
||||
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel bandwidth
|
||||
*/
|
||||
public void setBandwidth(Bandwidth bandwidth)
|
||||
{
|
||||
mBandwidth = bandwidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup to associate with audio produced by the NBFM decoder
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "talkgroup")
|
||||
public int getTalkgroup()
|
||||
{
|
||||
return mTalkgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the talkgroup identifier to attach to any demodulated audio streams
|
||||
* @param talkgroup (1-65,535)
|
||||
*/
|
||||
public void setTalkgroup(int talkgroup)
|
||||
{
|
||||
if(talkgroup < 1 || talkgroup > 65535)
|
||||
{
|
||||
throw new IllegalArgumentException("Valid talkgroup range is 1 - 65,535");
|
||||
}
|
||||
|
||||
mTalkgroup = talkgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets squelch threshold
|
||||
* @param threshold (dB)
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "squelch")
|
||||
@Override
|
||||
public void setSquelchThreshold(int threshold)
|
||||
{
|
||||
mSquelchThreshold = threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squelch threshold
|
||||
* @return threshold (dB)
|
||||
*/
|
||||
@Override
|
||||
public int getSquelchThreshold()
|
||||
{
|
||||
return mSquelchThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the squelch noise floor auto-track feature.
|
||||
* @param autoTrack true to enable.
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "autoTrack")
|
||||
@Override
|
||||
public void setSquelchAutoTrack(boolean autoTrack)
|
||||
{
|
||||
mSquelchAutoTrack = autoTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the squelch noise floor auto-track feature is enabled.
|
||||
* @return true if enabled.
|
||||
*/
|
||||
@Override
|
||||
public boolean isSquelchAutoTrack()
|
||||
{
|
||||
return mSquelchAutoTrack;
|
||||
}
|
||||
|
||||
public enum Bandwidth
|
||||
{
|
||||
BW_3_0("3.0 kHz", 3000.0),
|
||||
BW_5_0("5.0 kHz", 5000.0),
|
||||
BW_8_33("8.33 kHz", 8333.0),
|
||||
BW_12_5("12.5 kHz", 12500.0),
|
||||
BW_15_0("15.0 kHz", 15000.0),
|
||||
BW_25_0("25.0 kHz", 25000.0);
|
||||
|
||||
private String mLabel;
|
||||
private double mValue;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param label for display
|
||||
* @param value of the entry in Hertz
|
||||
*/
|
||||
Bandwidth(String label, double value)
|
||||
{
|
||||
mLabel = label;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
//AM demodulator channel bandwidth options
|
||||
public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0);
|
||||
|
||||
//FM demodulator channel bandwidth options
|
||||
public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_12_5, BW_25_0);
|
||||
|
||||
/**
|
||||
* Indicates if this entry is valid for the AM decoder.
|
||||
*/
|
||||
public boolean isAM()
|
||||
{
|
||||
return AM_BANDWIDTHS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this entry is valid for the FM decoder.
|
||||
*/
|
||||
public boolean isFM()
|
||||
{
|
||||
return FM_BANDWIDTHS.contains(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default toString to provide a pretty label.
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value of the bandwidth
|
||||
* @return value in Hertz
|
||||
*/
|
||||
public double getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.module.decode.analog;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.IDecoderStateEventProvider;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import io.github.dsheirer.dsp.fm.ISquelchingDemodulator;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
import io.github.dsheirer.sample.real.IRealBufferProvider;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract analog decoder module with integrated squelch control
|
||||
*/
|
||||
public abstract class SquelchingAnalogDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
|
||||
IComplexSamplesListener, Listener<ComplexSamples>, IRealBufferProvider,
|
||||
IDecoderStateEventProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SquelchingAnalogDecoder.class);
|
||||
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
|
||||
private IRealFilter mIBasebandFilter;
|
||||
private IRealFilter mQBasebandFilter;
|
||||
private IRealDecimationFilter mIDecimationFilter;
|
||||
private IRealDecimationFilter mQDecimationFilter;
|
||||
private ISquelchingDemodulator mDemodulator;
|
||||
private RealResampler mResampler;
|
||||
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||
private Listener<float[]> mResampledBufferListener;
|
||||
private Listener<DecoderStateEvent> mDecoderStateEventListener;
|
||||
private double mChannelBandwidth;
|
||||
private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE;
|
||||
protected boolean mSquelch = true;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param config to setup the decoder
|
||||
* @param squelchingDemodulator implementation (e.g. AM or FM).
|
||||
*/
|
||||
public SquelchingAnalogDecoder(DecodeConfigAnalog config, ISquelchingDemodulator squelchingDemodulator)
|
||||
{
|
||||
super(config);
|
||||
mDemodulator = squelchingDemodulator;
|
||||
mChannelBandwidth = config.getBandwidth().getValue();
|
||||
mDemodulator.setSquelchThreshold(config.getSquelchThreshold());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ISourceEventListener interface
|
||||
*/
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceEventProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module interface methods - unused.
|
||||
*/
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the demodulated audio samples to the registered listener.
|
||||
* @param demodulatedSamples to broadcast
|
||||
*/
|
||||
protected void broadcast(float[] demodulatedSamples)
|
||||
{
|
||||
if(mResampledBufferListener != null)
|
||||
{
|
||||
mResampledBufferListener.receive(demodulatedSamples);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IRealBufferProvider interface to register a listener for demodulated audio samples.
|
||||
*
|
||||
* @param listener to receive demodulated audio sample buffers.
|
||||
*/
|
||||
@Override
|
||||
public void setBufferListener(Listener<float[]> listener)
|
||||
{
|
||||
mResampledBufferListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IRealBufferProvider interface to deregister a listener from receiving demodulated audio samples.
|
||||
*/
|
||||
@Override
|
||||
public void removeBufferListener()
|
||||
{
|
||||
mResampledBufferListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IComplexSampleListener interface to receive a stream of complex sample buffers.
|
||||
*/
|
||||
@Override
|
||||
public Listener<ComplexSamples> getComplexSamplesListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Listener<ComplexSample> interface to receive a stream of complex sample buffers
|
||||
*/
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
if(mIDecimationFilter == null || mQDecimationFilter == null)
|
||||
{
|
||||
throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " +
|
||||
"event before it can process complex sample buffers");
|
||||
}
|
||||
|
||||
float[] decimatedI = mIDecimationFilter.decimateReal(samples.i());
|
||||
float[] decimatedQ = mQDecimationFilter.decimateReal(samples.q());
|
||||
|
||||
float[] filteredI = mIBasebandFilter.filter(decimatedI);
|
||||
float[] filteredQ = mQBasebandFilter.filter(decimatedQ);
|
||||
|
||||
float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ);
|
||||
|
||||
if(mResampler != null)
|
||||
{
|
||||
//Squelch changed while processing this audio buffer
|
||||
if(mDemodulator.isSquelchChanged())
|
||||
{
|
||||
if(mDemodulator.isMuted())
|
||||
{
|
||||
//Demodulator says to mute and we're already muted/squelched = Continue IDLE
|
||||
if(mSquelch)
|
||||
{
|
||||
notifyIdle();
|
||||
}
|
||||
//Demodulator says to mute and we are unmuted = Squelch and End Call
|
||||
else
|
||||
{
|
||||
mSquelch = true;
|
||||
notifyCallEnd();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Demodulator says to unmute and we're muted/squelched = Unmute and Start Call
|
||||
if(mSquelch)
|
||||
{
|
||||
mSquelch = false;
|
||||
notifyCallStart();
|
||||
}
|
||||
//Demodulator says to unmute and we're already unmuted = Continue Call
|
||||
else
|
||||
{
|
||||
notifyCallContinuation();
|
||||
}
|
||||
|
||||
mResampler.resample(demodulated);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Demodulator says squelch state didn't change and we're muted/squelched = Continue IDLE
|
||||
if(mSquelch)
|
||||
{
|
||||
notifyIdle();
|
||||
}
|
||||
//Demodulator says squelch state didn't change and we're unmuted/unsquelched = Continue CALL
|
||||
else
|
||||
{
|
||||
notifyCallContinuation();
|
||||
mResampler.resample(demodulated);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//This shouldn't happen
|
||||
notifyIdle();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call start state event
|
||||
*/
|
||||
protected void notifyCallStart()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.START, State.CALL, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call continuation state event
|
||||
*/
|
||||
private void notifyCallContinuation()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.CALL, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call end state event
|
||||
*/
|
||||
private void notifyCallEnd()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.END, State.CALL, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an idle notification
|
||||
*/
|
||||
private void notifyIdle()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.IDLE, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the decoder state event to an optional registered listener
|
||||
*/
|
||||
private void broadcast(DecoderStateEvent event)
|
||||
{
|
||||
if(mDecoderStateEventListener != null)
|
||||
{
|
||||
mDecoderStateEventListener.receive(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the decoder state listener
|
||||
*/
|
||||
@Override
|
||||
public void setDecoderStateListener(Listener<DecoderStateEvent> listener)
|
||||
{
|
||||
mDecoderStateEventListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the decoder state event listener
|
||||
*/
|
||||
@Override
|
||||
public void removeDecoderStateListener()
|
||||
{
|
||||
mDecoderStateEventListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
|
||||
*/
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mDemodulator.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* De-registers the listener
|
||||
*/
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mDemodulator.setSourceEventListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
|
||||
* change requests down to the demodulator.
|
||||
*/
|
||||
public class SourceEventProcessor implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
//Forward these requests to the sub-class implementation (AM or FM demod) to handle
|
||||
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
|
||||
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
|
||||
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
|
||||
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
|
||||
mDemodulator.receive(sourceEvent);
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
if(mIBasebandFilter != null)
|
||||
{
|
||||
mIBasebandFilter = null;
|
||||
mQBasebandFilter = null;
|
||||
}
|
||||
|
||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||
|
||||
int decimationRate = 0;
|
||||
double decimatedSampleRate = sampleRate;
|
||||
|
||||
if(sampleRate / 2 >= (mChannelBandwidth * 2))
|
||||
{
|
||||
decimationRate = 2;
|
||||
|
||||
while(sampleRate / decimationRate / 2 >= (mChannelBandwidth * 2))
|
||||
{
|
||||
decimationRate *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if(decimationRate > 0)
|
||||
{
|
||||
decimatedSampleRate /= decimationRate;
|
||||
}
|
||||
|
||||
mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
|
||||
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
|
||||
|
||||
if((decimatedSampleRate < (2.0 * mChannelBandwidth)))
|
||||
{
|
||||
throw new IllegalStateException(getDecoderType().name() + " demodulator with channel bandwidth [" +
|
||||
mChannelBandwidth + "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth +
|
||||
"] - sample rate of [" + decimatedSampleRate + "] is not supported"));
|
||||
}
|
||||
|
||||
mDemodulator.setSampleRate((int) decimatedSampleRate);
|
||||
|
||||
int passBandStop = (int) (mChannelBandwidth * .8);
|
||||
int stopBandStart = (int) mChannelBandwidth;
|
||||
|
||||
float[] coefficients = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(decimatedSampleRate * 2)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(passBandStop)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(stopBandStart)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.005) //Approximately 90 dB attenuation
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
coefficients = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design demodulator remez filter for sample rate [" + sampleRate +
|
||||
"] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
|
||||
"] - will proceed using sinc (low-pass) filter");
|
||||
}
|
||||
|
||||
if(coefficients == null)
|
||||
{
|
||||
mLog.info("Unable to use remez filter designer for sample rate [" + decimatedSampleRate +
|
||||
"] pass band stop [" + passBandStop +
|
||||
"] and stop band start [" + stopBandStart + "] - will proceed using simple low pass filter design");
|
||||
coefficients = FilterFactory.getLowPass(decimatedSampleRate, passBandStop, stopBandStart, 60,
|
||||
WindowType.HAMMING, true);
|
||||
}
|
||||
|
||||
mIBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
mQBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
|
||||
mResampler = new RealResampler(decimatedSampleRate, mOutputSampleRate, 4192, 512);
|
||||
mResampler.setListener(resampled -> broadcast(resampled));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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
|
||||
@@ -15,27 +14,21 @@
|
||||
*
|
||||
* 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.module.decode.nbfm;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.dsp.squelch.ISquelchConfiguration;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
||||
|
||||
/**
|
||||
* Decoder configuration for an NBFM channel
|
||||
*/
|
||||
public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchConfiguration
|
||||
public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
private Bandwidth mBandwidth = Bandwidth.BW_12_5;
|
||||
private Boolean mRecordAudio;
|
||||
private int mTalkgroup = 1;
|
||||
private int mSquelchThreshold = -60;
|
||||
|
||||
public DecodeConfigNBFM()
|
||||
{
|
||||
}
|
||||
@@ -46,116 +39,27 @@ public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchCon
|
||||
return DecoderType.NBFM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel bandwidth
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "bandwidth")
|
||||
public Bandwidth getBandwidth()
|
||||
@Override
|
||||
protected Bandwidth getDefaultBandwidth()
|
||||
{
|
||||
return mBandwidth;
|
||||
return Bandwidth.BW_12_5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel sample stream specification.
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public ChannelSpecification getChannelSpecification()
|
||||
{
|
||||
if(mBandwidth == Bandwidth.BW_12_5)
|
||||
switch(getBandwidth())
|
||||
{
|
||||
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel bandwidth
|
||||
*/
|
||||
public void setBandwidth(Bandwidth bandwidth)
|
||||
{
|
||||
mBandwidth = bandwidth;
|
||||
}
|
||||
|
||||
// @Deprecated //No longer used: 20211107
|
||||
// @JacksonXmlProperty(isAttribute = true, localName = "recordAudio")
|
||||
// public boolean getRecordAudio()
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// @Deprecated //No longer used: 20211107
|
||||
// public void setRecordAudio(boolean recordAudio)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
/**
|
||||
* Talkgroup to associate with audio produced by the NBFM decoder
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "talkgroup")
|
||||
public int getTalkgroup()
|
||||
{
|
||||
return mTalkgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the talkgroup identifier to attach to any demodulated audio streams
|
||||
* @param talkgroup (1-65,535)
|
||||
*/
|
||||
public void setTalkgroup(int talkgroup)
|
||||
{
|
||||
if(talkgroup < 1 || talkgroup > 65535)
|
||||
{
|
||||
throw new IllegalArgumentException("Valid talkgroup range is 1 - 65,535");
|
||||
}
|
||||
|
||||
mTalkgroup = talkgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets squelch threshold
|
||||
* @param threshold (dB)
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "squelch")
|
||||
@Override
|
||||
public void setSquelchThreshold(int threshold)
|
||||
{
|
||||
mSquelchThreshold = threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squelch threshold
|
||||
* @return threshold (dB)
|
||||
*/
|
||||
@Override
|
||||
public int getSquelchThreshold()
|
||||
{
|
||||
return mSquelchThreshold;
|
||||
}
|
||||
|
||||
public enum Bandwidth
|
||||
{
|
||||
BW_12_5("12.5 kHz", 12500.0),
|
||||
BW_25_0("25.0 kHz", 25000.0);
|
||||
|
||||
private String mLabel;
|
||||
private double mValue;
|
||||
|
||||
Bandwidth(String label, double value)
|
||||
{
|
||||
mLabel = label;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public double getValue()
|
||||
{
|
||||
return mValue;
|
||||
case BW_12_5:
|
||||
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
|
||||
case BW_25_0:
|
||||
return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized FM bandwidth value: " + getBandwidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 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
|
||||
@@ -18,352 +18,34 @@
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.IDecoderStateEventProvider;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import io.github.dsheirer.dsp.fm.FmDemodulatorFactory;
|
||||
import io.github.dsheirer.dsp.fm.ISquelchingFmDemodulator;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.dsp.fm.SquelchingFMDemodulator;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
import io.github.dsheirer.sample.real.IRealBufferProvider;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.module.decode.analog.SquelchingAnalogDecoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Decoder module with integrated narrowband FM (12.5 or 25.0 kHz channel) demodulator
|
||||
* Narrow Band FM Decoder module with integrated squelch control.
|
||||
*/
|
||||
public class NBFMDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
|
||||
IComplexSamplesListener, Listener<ComplexSamples>, IRealBufferProvider,
|
||||
IDecoderStateEventProvider
|
||||
public class NBFMDecoder extends SquelchingAnalogDecoder
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);
|
||||
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
|
||||
private static final float POWER_SQUELCH_ALPHA_DECAY = 0.0004f;
|
||||
private static final float POWER_SQUELCH_THRESHOLD_DB = -78.0f;
|
||||
private static final int POWER_SQUELCH_RAMP = 4;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);
|
||||
private static final float SQUELCH_ALPHA_DECAY = 0.0004f;
|
||||
|
||||
private IRealFilter mIBasebandFilter;
|
||||
private IRealFilter mQBasebandFilter;
|
||||
private IRealDecimationFilter mIDecimationFilter;
|
||||
private IRealDecimationFilter mQDecimationFilter;
|
||||
private ISquelchingFmDemodulator mDemodulator;
|
||||
private RealResampler mResampler;
|
||||
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||
private Listener<float[]> mResampledBufferListener;
|
||||
private Listener<DecoderStateEvent> mDecoderStateEventListener;
|
||||
private double mChannelBandwidth;
|
||||
private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE;
|
||||
private boolean mSquelch = true;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param config to setup the decoder
|
||||
*/
|
||||
public NBFMDecoder( DecodeConfigNBFM config )
|
||||
{
|
||||
super( config );
|
||||
mChannelBandwidth = config.getBandwidth().getValue();
|
||||
mDemodulator = FmDemodulatorFactory.getSquelchingFmDemodulator(POWER_SQUELCH_ALPHA_DECAY,
|
||||
POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
|
||||
mDemodulator.setSquelchThreshold(config.getSquelchThreshold());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param config to setup the decoder
|
||||
*/
|
||||
public NBFMDecoder(DecodeConfigNBFM config)
|
||||
{
|
||||
return DecoderType.NBFM;
|
||||
super(config, new SquelchingFMDemodulator(SQUELCH_ALPHA_DECAY, config.getSquelchThreshold(),
|
||||
config.isSquelchAutoTrack()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<ComplexSamples> getComplexSamplesListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceEventProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mDemodulator.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBufferListener(Listener<float[]> listener)
|
||||
{
|
||||
mResampledBufferListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeBufferListener()
|
||||
{
|
||||
mResampledBufferListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
if(mIDecimationFilter == null || mQDecimationFilter == null)
|
||||
{
|
||||
throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " +
|
||||
"event before it can process complex sample buffers");
|
||||
}
|
||||
|
||||
float[] decimatedI = mIDecimationFilter.decimateReal(samples.i());
|
||||
float[] decimatedQ = mQDecimationFilter.decimateReal(samples.q());
|
||||
|
||||
float[] filteredI = mIBasebandFilter.filter(decimatedI);
|
||||
float[] filteredQ = mQBasebandFilter.filter(decimatedQ);
|
||||
|
||||
float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ);
|
||||
|
||||
if(mResampler != null)
|
||||
{
|
||||
//If we're currently squelched and the squelch state changed while demodulating the baseband samples,
|
||||
// then un-squelch so we can send this buffer
|
||||
if(mSquelch && mDemodulator.isSquelchChanged())
|
||||
{
|
||||
mSquelch = false;
|
||||
notifyCallStart();
|
||||
}
|
||||
|
||||
//Either send the demodulated buffer to the resampler for distro, or decrement the user count
|
||||
if(mSquelch)
|
||||
{
|
||||
notifyIdle();
|
||||
}
|
||||
else
|
||||
{
|
||||
mResampler.resample(demodulated);
|
||||
notifyCallContinuation();
|
||||
}
|
||||
|
||||
//Set to squelch if necessary to close out the audio buffers
|
||||
if(!mSquelch && mDemodulator.isMuted())
|
||||
{
|
||||
mSquelch = true;
|
||||
notifyCallEnd();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
notifyIdle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call start state event
|
||||
*/
|
||||
private void notifyCallStart()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.START, State.CALL, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call continuation state event
|
||||
*/
|
||||
private void notifyCallContinuation()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.CALL, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call end state event
|
||||
*/
|
||||
private void notifyCallEnd()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.END, State.CALL, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an idle notification
|
||||
*/
|
||||
private void notifyIdle()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.IDLE, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the decoder state event to an optional registered listener
|
||||
*/
|
||||
private void broadcast(DecoderStateEvent event)
|
||||
{
|
||||
if(mDecoderStateEventListener != null)
|
||||
{
|
||||
mDecoderStateEventListener.receive(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the decoder state listener
|
||||
*/
|
||||
@Override
|
||||
public void setDecoderStateListener(Listener<DecoderStateEvent> listener)
|
||||
{
|
||||
mDecoderStateEventListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the decoder state event listener
|
||||
*/
|
||||
@Override
|
||||
public void removeDecoderStateListener()
|
||||
{
|
||||
mDecoderStateEventListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
|
||||
*/
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mDemodulator.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* De-registers the listener
|
||||
*/
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mDemodulator.setSourceEventListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
|
||||
* change requests down to the demodulator.
|
||||
*/
|
||||
public class SourceEventProcessor implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
|
||||
{
|
||||
if(mIBasebandFilter != null)
|
||||
{
|
||||
mIBasebandFilter = null;
|
||||
mQBasebandFilter = null;
|
||||
}
|
||||
|
||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||
|
||||
int decimationRate = 0;
|
||||
double decimatedSampleRate = sampleRate;
|
||||
|
||||
if(sampleRate / 2 >= (mChannelBandwidth * 2))
|
||||
{
|
||||
decimationRate = 2;
|
||||
|
||||
while(sampleRate / decimationRate / 2 >= (mChannelBandwidth * 2))
|
||||
{
|
||||
decimationRate *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if(decimationRate > 0)
|
||||
{
|
||||
decimatedSampleRate /= decimationRate;
|
||||
}
|
||||
|
||||
mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
|
||||
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
|
||||
|
||||
if((decimatedSampleRate < (2.0 * mChannelBandwidth)))
|
||||
{
|
||||
throw new IllegalStateException("FM Demodulator with channel bandwidth [" + mChannelBandwidth +
|
||||
"] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" +
|
||||
decimatedSampleRate + "] is not supported"));
|
||||
}
|
||||
|
||||
int passBandStop = (int)(mChannelBandwidth * .8);
|
||||
int stopBandStart = (int)mChannelBandwidth;
|
||||
|
||||
float[] coefficients = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(decimatedSampleRate * 2)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(passBandStop)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(stopBandStart)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.005) //Approximately 90 dB attenuation
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
coefficients = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate +
|
||||
"] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
|
||||
"] - will proceed using sinc (low-pass) filter");
|
||||
}
|
||||
|
||||
if(coefficients == null)
|
||||
{
|
||||
mLog.info("Unable to use remez filter designer for sample rate [" + decimatedSampleRate +
|
||||
"] pass band stop [" + passBandStop +
|
||||
"] and stop band start [" + stopBandStart + "] - will proceed using simple low pass filter design");
|
||||
coefficients = FilterFactory.getLowPass(decimatedSampleRate, passBandStop, stopBandStart, 60,
|
||||
WindowType.HAMMING, true);
|
||||
}
|
||||
|
||||
mIBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
mQBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
|
||||
mResampler = new RealResampler(decimatedSampleRate, mOutputSampleRate, 4192, 512);
|
||||
|
||||
mResampler.setListener(resampled ->
|
||||
{
|
||||
if(mResampledBufferListener != null)
|
||||
{
|
||||
mResampledBufferListener.receive(resampled);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
|
||||
{
|
||||
//Send request for squelch threshold change to the demodulator/squelch controller.
|
||||
mDemodulator.receive(sourceEvent);
|
||||
}
|
||||
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
|
||||
{
|
||||
mDemodulator.receive(sourceEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.NBFM;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -16,42 +16,31 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
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;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.module.decode.analog.AnalogDecoderState;
|
||||
|
||||
/**
|
||||
* Narrow Band FM decoder channel state - provides the minimum channel state functionality
|
||||
* NBFM decoder state
|
||||
*/
|
||||
public class NBFMDecoderState extends DecoderState implements ISourceEventListener
|
||||
public class NBFMDecoderState extends AnalogDecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoderState.class);
|
||||
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
|
||||
private String mChannelName;
|
||||
private DecodeEvent mDecodeEvent;
|
||||
private Identifier mChannelNameIdentifier;
|
||||
private Identifier mTalkgroupIdentifier;
|
||||
private IChannelDescriptor mChannelDescriptor = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param channelName to use for this channel
|
||||
* @param decodeConfig with talkgroup identifier
|
||||
*/
|
||||
public NBFMDecoderState(String channelName, DecodeConfigNBFM decodeConfig)
|
||||
{
|
||||
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "NBFM CHANNEL";
|
||||
@@ -59,116 +48,6 @@ public class NBFMDecoderState extends DecoderState implements ISourceEventListen
|
||||
mTalkgroupIdentifier = new NBFMTalkgroup(decodeConfig.getTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Activity Summary\n");
|
||||
sb.append("\tDecoder: NBFM");
|
||||
sb.append("\n\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(IMessage t)
|
||||
{
|
||||
/* Not implemented */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case REQUEST_RESET ->
|
||||
{
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
case START ->
|
||||
{
|
||||
if(event.getState() == State.CALL)
|
||||
{
|
||||
startCallEvent();
|
||||
}
|
||||
}
|
||||
case END ->
|
||||
{
|
||||
if(event.getState() == State.CALL)
|
||||
{
|
||||
endCallEvent();
|
||||
}
|
||||
}
|
||||
case CONTINUATION ->
|
||||
{
|
||||
if(event.getState() == State.CALL)
|
||||
{
|
||||
continueCallEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
endCallEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/starts a decode call evnet.
|
||||
*/
|
||||
private void startCallEvent()
|
||||
{
|
||||
getIdentifierCollection().update(mTalkgroupIdentifier);
|
||||
|
||||
if(mDecodeEvent == null)
|
||||
{
|
||||
mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis())
|
||||
.channel(mChannelDescriptor)
|
||||
.eventDescription("CALL")
|
||||
.details("NBFM")
|
||||
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
|
||||
.build();
|
||||
|
||||
broadcast(mDecodeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues (or starts) the call decode event and updates the current timestamp
|
||||
*/
|
||||
private void continueCallEvent()
|
||||
{
|
||||
if(mDecodeEvent == null)
|
||||
{
|
||||
startCallEvent();
|
||||
}
|
||||
|
||||
mDecodeEvent.update(System.currentTimeMillis());
|
||||
broadcast(mDecodeEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the call decode event
|
||||
*/
|
||||
private void endCallEvent()
|
||||
{
|
||||
if(mDecodeEvent != null)
|
||||
{
|
||||
mDecodeEvent.end(System.currentTimeMillis());
|
||||
broadcast(mDecodeEvent);
|
||||
mDecodeEvent = null;
|
||||
}
|
||||
|
||||
//Remote any user identifiers from the identifier collection
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
@@ -176,34 +55,24 @@ public class NBFMDecoderState extends DecoderState implements ISourceEventListen
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
protected Identifier getChannelNameIdentifier()
|
||||
{
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
return mChannelNameIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
protected Identifier getTalkgroupIdentifier()
|
||||
{
|
||||
return mTalkgroupIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
public String getActivitySummary()
|
||||
{
|
||||
return mSourceEventListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors source events to capture the channel frequency for use in decode events.
|
||||
*/
|
||||
private class SourceEventListener implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
|
||||
{
|
||||
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Activity Summary\n");
|
||||
sb.append("\tDecoder: NBFM");
|
||||
sb.append("\n\n");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 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.module.demodulate.am;
|
||||
|
||||
import io.github.dsheirer.dsp.am.AMDemodulator;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import io.github.dsheirer.dsp.gain.AutomaticGainControl;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
import io.github.dsheirer.sample.real.IRealBufferProvider;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* AM Demodulator Module for demodulating complex baseband channel sample buffers and producing demodulated audio
|
||||
* at a specified output sample rate, normally 8 kHz.
|
||||
* <p>
|
||||
* This module requires a sample rate SourceEvent prior to processing baseband sample buffers in order to configure
|
||||
* internal filters and the resampler.
|
||||
*/
|
||||
public class AMDemodulatorModule extends Module
|
||||
implements ISourceEventListener, IComplexSamplesListener, IRealBufferProvider, Listener<ComplexSamples>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AMDemodulatorModule.class);
|
||||
private IRealFilter mIBasebandFilter;
|
||||
private IRealFilter mQBasebandFilter;
|
||||
|
||||
private AMDemodulator mDemodulator;
|
||||
private IRealFilter mLowPassFilter;
|
||||
private AutomaticGainControl mAGC = new AutomaticGainControl();
|
||||
private double mChannelBandwidth;
|
||||
private double mOutputSampleRate;
|
||||
private RealResampler mResampler;
|
||||
private Listener<float[]> mResampledReusableBufferListener;
|
||||
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||
|
||||
/**
|
||||
* Constructs an AM demodulator module
|
||||
*
|
||||
* @param channelBandwidth to use in filtering the baseband input buffers
|
||||
* @param outputSampleRate specifies the resampled output sample rate for demodulated audio
|
||||
*/
|
||||
public AMDemodulatorModule(double channelBandwidth, double outputSampleRate)
|
||||
{
|
||||
mChannelBandwidth = channelBandwidth;
|
||||
mOutputSampleRate = outputSampleRate;
|
||||
|
||||
mDemodulator = new AMDemodulator(500.0f);
|
||||
|
||||
float[] lowPassCoefficients = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder().sampleRate(mOutputSampleRate)
|
||||
.gridDensity(16).oddLength(true).passBandCutoff(3000).passBandAmplitude(1.0).passBandRipple(0.01)
|
||||
.stopBandStart(3500).stopBandAmplitude(0.0).stopBandRipple(0.028).build();
|
||||
|
||||
try
|
||||
{
|
||||
lowPassCoefficients = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design AM demodulator remez filter for sample rate [" + mOutputSampleRate
|
||||
+ "] pass frequency [3000] and stop frequency [3500] - using sinc filter");
|
||||
}
|
||||
|
||||
if(lowPassCoefficients == null)
|
||||
{
|
||||
lowPassCoefficients = FilterFactory.getLowPass(mOutputSampleRate, 3000, 3500, 60, WindowType.HAMMING, true);
|
||||
}
|
||||
|
||||
mLowPassFilter = FilterFactory.getRealFilter(lowPassCoefficients);
|
||||
}
|
||||
|
||||
@Override public void receive(ComplexSamples samples)
|
||||
{
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
float[] q = mQBasebandFilter.filter(samples.q());
|
||||
float[] demodulated = mDemodulator.demodulate(i, q);
|
||||
mResampler.resample(demodulated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes resampled audio buffers produced by the resampler.
|
||||
*
|
||||
* @param resampledAudio buffer to process.
|
||||
*/
|
||||
private void processResampledAudio(float[] resampledAudio)
|
||||
{
|
||||
if(mResampledReusableBufferListener != null)
|
||||
{
|
||||
float[] filtered = mLowPassFilter.filter(resampledAudio);
|
||||
float[] amplified = mAGC.process(filtered);
|
||||
mResampledReusableBufferListener.receive(amplified);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Listener<ComplexSamples> getComplexSamplesListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@Override public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override public void setBufferListener(Listener<float[]> listener)
|
||||
{
|
||||
mResampledReusableBufferListener = listener;
|
||||
}
|
||||
|
||||
@Override public void removeBufferListener()
|
||||
{
|
||||
mResampledReusableBufferListener = null;
|
||||
}
|
||||
|
||||
@Override public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceEventProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors sample rate change source event(s) to setup the initial I/Q filter and resampler
|
||||
*/
|
||||
public class SourceEventProcessor implements Listener<SourceEvent>
|
||||
{
|
||||
@Override public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
|
||||
{
|
||||
if(mIBasebandFilter != null || mQBasebandFilter != null)
|
||||
{
|
||||
mIBasebandFilter = null;
|
||||
mQBasebandFilter = null;
|
||||
}
|
||||
|
||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||
|
||||
if((sampleRate < (2.0 * mChannelBandwidth)))
|
||||
{
|
||||
throw new IllegalStateException("AM Demodulator with channel bandwidth [" + mChannelBandwidth
|
||||
+ "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth
|
||||
+ "] - sample rate of [" + sampleRate + "] is not supported"));
|
||||
}
|
||||
|
||||
double cutoff = sampleRate / 4.0;
|
||||
int passBandStop = (int)cutoff - 500;
|
||||
int stopBandStart = (int)cutoff + 500;
|
||||
|
||||
float[] coefficients = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder().sampleRate(sampleRate)
|
||||
.gridDensity(16).oddLength(true).passBandCutoff(passBandStop).passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01).stopBandStart(stopBandStart).stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.028) //Approximately 60 dB attenuation
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
coefficients = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design AM demodulator remez filter for sample rate [" + sampleRate
|
||||
+ "] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart
|
||||
+ "] - using sinc filter");
|
||||
}
|
||||
|
||||
if(coefficients == null)
|
||||
{
|
||||
coefficients = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60,
|
||||
WindowType.HAMMING, true);
|
||||
}
|
||||
|
||||
mIBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
mQBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
mResampler = new RealResampler(sampleRate, mOutputSampleRate, 8192, 512);
|
||||
mResampler.setListener(resampledBuffer -> processResampledAudio(resampledBuffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user