mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 23:57:25 -05:00
Compare commits
129 Commits
v0.3.4-bet
...
v0.4.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9176ca610f | ||
|
|
97b31e4d9b | ||
|
|
a3e5520d3e | ||
|
|
8c456972cc | ||
|
|
35d81ab31d | ||
|
|
b90b06e600 | ||
|
|
04144ca677 | ||
|
|
d3c7a50918 | ||
|
|
0fd29fa35a | ||
|
|
c00b173064 | ||
|
|
2718367259 | ||
|
|
3f0c13096a | ||
|
|
71ba4adbd7 | ||
|
|
3908e29a2e | ||
|
|
0c47efea03 | ||
|
|
5c0d97d942 | ||
|
|
61ae0d2d83 | ||
|
|
68f2a8da2c | ||
|
|
0d69f11e65 | ||
|
|
b3d5daea8b | ||
|
|
fe34623c4a | ||
|
|
353747cecc | ||
|
|
a8c649ef64 | ||
|
|
8cf50e629e | ||
|
|
20a8f2bb3c | ||
|
|
8c237be682 | ||
|
|
2d5271b849 | ||
|
|
609a1e86c7 | ||
|
|
821e719015 | ||
|
|
104c4f72be | ||
|
|
9e7fa7618d | ||
|
|
ee09520181 | ||
|
|
fa12b840db | ||
|
|
183d7d6d19 | ||
|
|
e2550869ce | ||
|
|
e88944e53d | ||
|
|
0051a1d381 | ||
|
|
f7a873dd0c | ||
|
|
ad560db5ea | ||
|
|
6f1ce79461 | ||
|
|
df1bc1031e | ||
|
|
a621a31aa9 | ||
|
|
19fa256c60 | ||
|
|
7a830077bd | ||
|
|
f4793f3cfd | ||
|
|
2cd3b73633 | ||
|
|
18f50485d3 | ||
|
|
b410cf0dfe | ||
|
|
1fdcb623a6 | ||
|
|
9dda1f95a1 | ||
|
|
a5c8552937 | ||
|
|
e4209c8e4f | ||
|
|
a898be265f | ||
|
|
c13315f731 | ||
|
|
341e740e77 | ||
|
|
cbc228d798 | ||
|
|
488b2e02ce | ||
|
|
e395d737f8 | ||
|
|
7a9f705517 | ||
|
|
e971ad4582 | ||
|
|
ff9395e4b9 | ||
|
|
d023da9ead | ||
|
|
60ffb18cd9 | ||
|
|
c36f6901af | ||
|
|
bfaacb309d | ||
|
|
c3b405a752 | ||
|
|
28da8b2fa4 | ||
|
|
28cb9d9407 | ||
|
|
96d9d6f750 | ||
|
|
85691edacb | ||
|
|
f2f2c92afe | ||
|
|
fcd3c2fd46 | ||
|
|
e78d2339ff | ||
|
|
673c8a2423 | ||
|
|
5a03a0038c | ||
|
|
e7f7d27a1f | ||
|
|
0cf964fa98 | ||
|
|
b6f282a85a | ||
|
|
32ab0598e8 | ||
|
|
5ae3cb38e1 | ||
|
|
78205bb1bf | ||
|
|
a7a0c26b38 | ||
|
|
d4761be00a | ||
|
|
05fa5c25fd | ||
|
|
d2e7f008c8 | ||
|
|
236df8f3f4 | ||
|
|
667c50174d | ||
|
|
1c71a37755 | ||
|
|
318e6f3586 | ||
|
|
f38184fd83 | ||
|
|
d2085052f6 | ||
|
|
441b595325 | ||
|
|
57fab2b7ca | ||
|
|
bf65e10286 | ||
|
|
a12aaf7cf3 | ||
|
|
b3e4bd6a9c | ||
|
|
5281679b7f | ||
|
|
a581da56a8 | ||
|
|
0181926fe3 | ||
|
|
2c3be02939 | ||
|
|
a00ec42230 | ||
|
|
14a3532030 | ||
|
|
3be65d5e9a | ||
|
|
c21f3fca23 | ||
|
|
99d81d247a | ||
|
|
27461df7f0 | ||
|
|
9983d2119d | ||
|
|
9e275b7d62 | ||
|
|
f461ed9cdb | ||
|
|
7c6b7494b8 | ||
|
|
ab31b40124 | ||
|
|
ac686f5f64 | ||
|
|
f7cd798f07 | ||
|
|
41d307cdad | ||
|
|
afae1654d5 | ||
|
|
ffc92a8f0e | ||
|
|
6d9e129087 | ||
|
|
0e1da8fb3b | ||
|
|
7d5371db4c | ||
|
|
131290fcd2 | ||
|
|
e2fe06638b | ||
|
|
17c16e16ee | ||
|
|
cd4eaedfcc | ||
|
|
fd748fec79 | ||
|
|
14b844f0bd | ||
|
|
99ef002d07 | ||
|
|
e868428c88 | ||
|
|
d2542d2560 | ||
|
|
9ca87299d5 |
32
.idea/dictionaries/denny.xml
generated
Normal file
32
.idea/dictionaries/denny.xml
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="denny">
|
||||
<words>
|
||||
<w>bahroo</w>
|
||||
<w>broadcastify</w>
|
||||
<w>correlator</w>
|
||||
<w>dinf</w>
|
||||
<w>esnid</w>
|
||||
<w>fleetsync</w>
|
||||
<w>heterdyned</w>
|
||||
<w>heterodynes</w>
|
||||
<w>icecast</w>
|
||||
<w>ident</w>
|
||||
<w>nutall</w>
|
||||
<w>overflowable</w>
|
||||
<w>polyphase</w>
|
||||
<w>qpsk</w>
|
||||
<w>remez</w>
|
||||
<w>resampler</w>
|
||||
<w>samplerate</w>
|
||||
<w>sdrtrunk</w>
|
||||
<w>sheirer</w>
|
||||
<w>shoutcast</w>
|
||||
<w>streamable</w>
|
||||
<w>talkgroup</w>
|
||||
<w>tgid</w>
|
||||
<w>ultravox</w>
|
||||
<w>wacn</w>
|
||||
<w>xtea</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
14
.idea/libraries/imports.xml
generated
Normal file
14
.idea/libraries/imports.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<component name="libraryTable">
|
||||
<library name="imports">
|
||||
<CLASSES>
|
||||
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/java-lame-3.98.4.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/radioreference-14.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/wsdl4j-1.5.1.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/commons-math3-3.6.1.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/libresample4j-0.1.3.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/vcs.xml
generated
10
.idea/vcs.xml
generated
@@ -1,5 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="IssueNavigationConfiguration">
|
||||
<option name="links">
|
||||
<list>
|
||||
<IssueNavigationLink>
|
||||
<option name="issueRegexp" value="#(\d+)" />
|
||||
<option name="linkRegexp" value="https://github.com/DSheirer/sdrtrunk/issues/$1" />
|
||||
</IssueNavigationLink>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
|
||||
@@ -15,7 +15,7 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
version = '0.3.4-beta.2'
|
||||
version = '0.4.0-alpha.2'
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
BIN
imports/commons-math3-3.6.1.jar
Normal file
BIN
imports/commons-math3-3.6.1.jar
Normal file
Binary file not shown.
BIN
imports/libresample4j-0.1.3.jar
Normal file
BIN
imports/libresample4j-0.1.3.jar
Normal file
Binary file not shown.
14
sdrtrunk.iml
Normal file
14
sdrtrunk.iml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/images" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/config" type="java-resource" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="imports" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.output.StereoAudioOutput;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
@@ -43,7 +44,7 @@ import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
public class AudioManager implements Listener<ReusableAudioPacket>, IAudioController
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioManager.class);
|
||||
|
||||
@@ -58,7 +59,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
||||
|
||||
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
|
||||
private LinkedTransferQueue<ReusableAudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
|
||||
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
|
||||
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
|
||||
private AudioOutputConnection mLowestPriorityConnection;
|
||||
@@ -170,7 +171,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void receive(AudioPacket packet)
|
||||
public synchronized void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
mAudioPacketQueue.add(packet);
|
||||
}
|
||||
@@ -362,7 +363,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
* @param audioPacket from a decoding channel source
|
||||
* @return an audio output connection or null
|
||||
*/
|
||||
private AudioOutputConnection getConnection(AudioPacket audioPacket)
|
||||
private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
|
||||
{
|
||||
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
|
||||
|
||||
@@ -410,6 +411,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
|
||||
public class AudioPacketProcessor implements Runnable
|
||||
{
|
||||
private List<ReusableAudioPacket> mPackets = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
@@ -419,14 +422,12 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
|
||||
if(mAudioPacketQueue != null)
|
||||
{
|
||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
||||
mAudioPacketQueue.drainTo(mPackets);
|
||||
|
||||
mAudioPacketQueue.drainTo(packets);
|
||||
|
||||
for(AudioPacket packet : packets)
|
||||
for(ReusableAudioPacket packet : mPackets)
|
||||
{
|
||||
/* Don't process any packet's marked as do not monitor */
|
||||
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO)
|
||||
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
AudioOutputConnection connection = getConnection(packet);
|
||||
|
||||
@@ -434,13 +435,29 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
{
|
||||
connection.receive(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
mPackets.clear();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Encountered error while processing audio packets", e);
|
||||
|
||||
while(mPackets.size() > 0)
|
||||
{
|
||||
ReusableAudioPacket audioPacket = mPackets.remove(0);
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +480,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
mAudioOutput = audioOutput;
|
||||
}
|
||||
|
||||
public void receive(AudioPacket packet)
|
||||
public void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
if(packet.hasMetadata() && packet.getMetadata().getMetadataID() == mChannelMetadataID)
|
||||
{
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 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.
|
||||
* 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/>
|
||||
* 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.audio;
|
||||
@@ -23,92 +20,80 @@ import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Provides packaging of demodulated audio sample buffers into audio packets for
|
||||
* broadcast to registered audio packet listeners. Includes audio packet
|
||||
* metadata in constructed audio packets.
|
||||
* Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet
|
||||
* listeners. Includes audio packet metadata in constructed audio packets.
|
||||
*
|
||||
* Incorporates audio squelch state listener to control if audio packets are
|
||||
* broadcast or ignored.
|
||||
* Incorporates audio squelch state listener to control if audio packets are broadcast or ignored.
|
||||
*
|
||||
* This class is designed to support 8 kHz sample rate demodulated audio.
|
||||
*/
|
||||
public class AudioModule extends Module implements IAudioPacketProvider, IFilteredRealBufferListener,
|
||||
ISquelchStateListener, Listener<RealBuffer>
|
||||
public class AudioModule extends Module implements IAudioPacketProvider, IReusableBufferListener,
|
||||
ISquelchStateListener, Listener<ReusableBuffer>
|
||||
{
|
||||
protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule");
|
||||
|
||||
private static float[] mDecimationCoefficients;
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
//Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling).
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
|
||||
.sampleRate(48000)
|
||||
.stopFrequency1(150)
|
||||
.passFrequencyBegin(300)
|
||||
.passFrequencyEnd(3500)
|
||||
.stopFrequency2(4000)
|
||||
.stopRipple(0.0004)
|
||||
.passRipple(0.008)
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
try
|
||||
{
|
||||
if(!designer.isValid())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't design the audio decimation filter");
|
||||
}
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
mDecimationCoefficients = designer.getImpulseResponse();
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException e)
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.debug("Error designing filter", e);
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients);
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
private Listener<AudioPacket> mAudioPacketListener;
|
||||
private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter;
|
||||
private Listener<ReusableAudioPacket> mAudioPacketListener;
|
||||
private Metadata mMetadata;
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
*
|
||||
* @param metadata to use for audio packets produced by this audio module.
|
||||
*/
|
||||
public AudioModule(Metadata metadata)
|
||||
{
|
||||
mMetadata = metadata;
|
||||
|
||||
mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f);
|
||||
mAudioDecimationFilter.setListener(new Listener<RealBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(RealBuffer realBuffer)
|
||||
{
|
||||
if(mAudioPacketListener != null)
|
||||
{
|
||||
AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf());
|
||||
mAudioPacketListener.receive(packet);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mSquelchStateListener = null;
|
||||
mAudioPacketListener = null;
|
||||
mSquelchStateListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,7 +102,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
/* No start operations provided */
|
||||
}
|
||||
@@ -125,34 +110,18 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
/* Issue an end audio packet in case a recorder is still rolling */
|
||||
/* Issue an end-audio packet in case a recorder is still rolling */
|
||||
if(mAudioPacketListener != null)
|
||||
{
|
||||
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes demodulated audio samples into audio packets with current audio
|
||||
* metadata and sends to the registered listener
|
||||
*/
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
{
|
||||
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
mAudioDecimationFilter.receive(buffer);
|
||||
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
|
||||
endAudioPacket.setMetadata(mMetadata.copyOf());
|
||||
endAudioPacket.incrementUserCount();
|
||||
mAudioPacketListener.receive(endAudioPacket);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAudioPacketListener(Listener<AudioPacket> listener)
|
||||
public void setAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
||||
{
|
||||
mAudioPacketListener = listener;
|
||||
}
|
||||
@@ -169,6 +138,34 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
|
||||
return mSquelchStateListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableBuffer reusableBuffer)
|
||||
{
|
||||
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
ReusableBuffer highPassFiltered = mHighPassFilter.filter(reusableBuffer);
|
||||
|
||||
ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount());
|
||||
audioPacket.loadAudioFrom(highPassFiltered);
|
||||
audioPacket.setMetadata(mMetadata.copyOf());
|
||||
|
||||
mAudioPacketListener.receive(audioPacket);
|
||||
|
||||
highPassFiltered.decrementUserCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
reusableBuffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener getReusableBufferListener()
|
||||
{
|
||||
//Redirect received reusable buffers to the receive(buffer) method
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for squelch state listener
|
||||
*/
|
||||
@@ -179,7 +176,9 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
|
||||
{
|
||||
if(state == SquelchState.SQUELCH && mAudioPacketListener != null)
|
||||
{
|
||||
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
|
||||
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
|
||||
endAudioPacket.setMetadata(mMetadata.copyOf());
|
||||
mAudioPacketListener.receive(endAudioPacket);
|
||||
}
|
||||
|
||||
mSquelchState = state;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.audio;
|
||||
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class AudioPacket
|
||||
{
|
||||
private Type mType;
|
||||
private RealBuffer mAudioData;
|
||||
private Metadata mMetadata;
|
||||
|
||||
public AudioPacket(Type type, Metadata metadata)
|
||||
{
|
||||
mType = type;
|
||||
mMetadata = metadata;
|
||||
}
|
||||
|
||||
public AudioPacket(float[] audio, Metadata metadata)
|
||||
{
|
||||
this(Type.AUDIO, metadata);
|
||||
|
||||
mAudioData = new RealBuffer(audio);
|
||||
}
|
||||
|
||||
public boolean hasMetadata()
|
||||
{
|
||||
return mMetadata != null;
|
||||
}
|
||||
|
||||
public Metadata getMetadata()
|
||||
{
|
||||
return mMetadata;
|
||||
}
|
||||
|
||||
public void setMetadata(Metadata metadata)
|
||||
{
|
||||
mMetadata = metadata;
|
||||
}
|
||||
|
||||
public Type getType()
|
||||
{
|
||||
return mType;
|
||||
}
|
||||
|
||||
public RealBuffer getAudioBuffer()
|
||||
{
|
||||
return mAudioData;
|
||||
}
|
||||
|
||||
public boolean hasAudioBuffer()
|
||||
{
|
||||
return mAudioData != null;
|
||||
}
|
||||
|
||||
public enum Type
|
||||
{
|
||||
AUDIO,
|
||||
END;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AudioPacketRateCounter implements Listener<AudioPacket>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( AudioPacketRateCounter.class );
|
||||
|
||||
private Listener<AudioPacket> mListener;
|
||||
private long mCount;
|
||||
private long mStartTime = -1;
|
||||
|
||||
private String mName;
|
||||
|
||||
public AudioPacketRateCounter()
|
||||
{
|
||||
mName = "UNNAMED";
|
||||
}
|
||||
|
||||
public AudioPacketRateCounter( String name )
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( AudioPacket packet )
|
||||
{
|
||||
if( mStartTime == -1 )
|
||||
{
|
||||
mStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
mCount += packet.getAudioBuffer().getSamples().length;
|
||||
|
||||
long elapsed = System.currentTimeMillis() - mStartTime;
|
||||
|
||||
if( elapsed != 0 )
|
||||
{
|
||||
mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" );
|
||||
}
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( packet );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<AudioPacket> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,8 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
|
||||
|
||||
private static ImageIcon MUTED_ICON = IconManager.getScaledIcon("/images/audio_muted.png", 20);
|
||||
private static ImageIcon UNMUTED_ICON = IconManager.getScaledIcon("/images/audio_unmuted.png", 20);
|
||||
private static ImageIcon MUTED_ICON = IconManager.getScaledIcon("images/audio_muted.png", 20);
|
||||
private static ImageIcon UNMUTED_ICON = IconManager.getScaledIcon("images/audio_unmuted.png", 20);
|
||||
|
||||
private IconManager mIconManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 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.
|
||||
* 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/>
|
||||
* 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.audio;
|
||||
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -34,17 +32,18 @@ public class AudioUtils
|
||||
/**
|
||||
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
|
||||
*/
|
||||
public static byte[] convertTo16BitSamples(List<AudioPacket> audioPackets)
|
||||
public static byte[] convertTo16BitSamples(List<ReusableAudioPacket> audioPackets)
|
||||
{
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
for(AudioPacket packet: audioPackets)
|
||||
for(ReusableAudioPacket audioPacket: audioPackets)
|
||||
{
|
||||
//Converting from 32-bit floats to signed 16-bit samples
|
||||
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
|
||||
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples());
|
||||
stream.write(buffer.array());
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
}
|
||||
catch(IOException e)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
|
||||
public interface IAudioPacketListener
|
||||
{
|
||||
public Listener<AudioPacket> getAudioPacketListener();
|
||||
public Listener<ReusableAudioPacket> getAudioPacketListener();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
|
||||
public interface IAudioPacketProvider
|
||||
{
|
||||
public void setAudioPacketListener( Listener<AudioPacket> listener );
|
||||
public void setAudioPacketListener( Listener<ReusableAudioPacket> listener );
|
||||
public void removeAudioPacketListener();
|
||||
}
|
||||
|
||||
@@ -245,7 +245,11 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
|
||||
{
|
||||
if(mBroadcastState != state)
|
||||
{
|
||||
mLog.info("[" + getStreamName() + "] status: " + state);
|
||||
if(state == BroadcastState.CONNECTED || state == BroadcastState.DISCONNECTED)
|
||||
{
|
||||
mLog.info("[" + getStreamName() + "] status: " + state);
|
||||
}
|
||||
|
||||
mBroadcastState = state;
|
||||
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -46,7 +46,7 @@ import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<ReusableAudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
|
||||
|
||||
@@ -276,7 +276,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket audioPacket)
|
||||
public void receive(ReusableAudioPacket audioPacket)
|
||||
{
|
||||
if(audioPacket.hasMetadata() && audioPacket.getMetadata().isStreamable())
|
||||
{
|
||||
@@ -284,11 +284,14 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
{
|
||||
if(mBroadcasterMap.containsKey(channel.getChannelName()))
|
||||
{
|
||||
audioPacket.incrementUserCount();
|
||||
mStreamManager.receive(audioPacket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,6 +70,11 @@ public enum BroadcastState
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
|
||||
/**
|
||||
* Network is unavailable or the server address cannot be resolved
|
||||
*/
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false),
|
||||
|
||||
/**
|
||||
* Server is not known or reachable
|
||||
*/
|
||||
|
||||
@@ -20,9 +20,14 @@ package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.*;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
|
||||
public class BroadcastStatusPanel extends JPanel
|
||||
{
|
||||
@@ -96,7 +101,8 @@ public class BroadcastStatusPanel extends JPanel
|
||||
setBackground(table.getBackground());
|
||||
setForeground(Color.LIGHT_GRAY);
|
||||
}
|
||||
else if(state == BroadcastState.INVALID_SETTINGS)
|
||||
else if(state == BroadcastState.INVALID_SETTINGS ||
|
||||
state == BroadcastState.NETWORK_UNAVAILABLE)
|
||||
{
|
||||
setBackground(Color.YELLOW);
|
||||
setForeground(table.getForeground());
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.record.AudioRecorder;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class StreamManager implements Listener<AudioPacket>
|
||||
public class StreamManager implements Listener<ReusableAudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
|
||||
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
|
||||
@@ -115,7 +115,7 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket audioPacket)
|
||||
public void receive(ReusableAudioPacket audioPacket)
|
||||
{
|
||||
if(mRunning.get() && audioPacket.hasMetadata())
|
||||
{
|
||||
@@ -123,9 +123,9 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
{
|
||||
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
|
||||
|
||||
AudioPacket.Type type = audioPacket.getType();
|
||||
ReusableAudioPacket.Type type = audioPacket.getType();
|
||||
|
||||
if(type == AudioPacket.Type.AUDIO)
|
||||
if(type == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
if(mStreamRecorders.containsKey(channelMetadataID))
|
||||
{
|
||||
@@ -133,6 +133,7 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
|
||||
if(recorder != null)
|
||||
{
|
||||
audioPacket.incrementUserCount();
|
||||
recorder.receive(audioPacket);
|
||||
}
|
||||
}
|
||||
@@ -140,12 +141,13 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
{
|
||||
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
|
||||
mBroadcastFormat);
|
||||
recorder.start(ThreadPool.SCHEDULED);
|
||||
recorder.start();
|
||||
audioPacket.incrementUserCount();
|
||||
recorder.receive(audioPacket);
|
||||
mStreamRecorders.put(channelMetadataID, recorder);
|
||||
}
|
||||
}
|
||||
else if(type == AudioPacket.Type.END)
|
||||
else if(type == ReusableAudioPacket.Type.END)
|
||||
{
|
||||
removeRecorder(channelMetadataID);
|
||||
}
|
||||
@@ -155,6 +157,8 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
@@ -35,7 +36,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
@@ -117,70 +117,64 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
||||
{
|
||||
mMetadataQueue.offer(getSong(metadata));
|
||||
|
||||
if(mUpdating.compareAndSet(false, true))
|
||||
if(!mUpdating.get() && !mMetadataQueue.isEmpty())
|
||||
{
|
||||
String song = mMetadataQueue.poll();
|
||||
|
||||
while(song != null)
|
||||
Runnable runnable = new Runnable()
|
||||
{
|
||||
HttpRequest updateRequest = createUpdateRequest(song);
|
||||
|
||||
if(updateRequest != null)
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
ThreadPool.SCHEDULED.schedule(new Runnable()
|
||||
if(!mMetadataQueue.isEmpty() && mUpdating.compareAndSet(false, true))
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectFuture connectFuture = getSocketConnector()
|
||||
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
||||
mIcecastConfiguration.getPort()));
|
||||
connectFuture.awaitUninterruptibly();
|
||||
IoSession session = connectFuture.getSession();
|
||||
ConnectFuture connectFuture = getSocketConnector()
|
||||
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
||||
mIcecastConfiguration.getPort()));
|
||||
connectFuture.awaitUninterruptibly();
|
||||
IoSession session = connectFuture.getSession();
|
||||
|
||||
if(session != null)
|
||||
{
|
||||
session.write(updateRequest);
|
||||
}
|
||||
}
|
||||
catch(UnresolvedAddressException uae)
|
||||
if(session != null)
|
||||
{
|
||||
//Do nothing - the server is temporarily unavailable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
String song = mMetadataQueue.poll();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
while(song != null)
|
||||
{
|
||||
//Do nothing, the server is unavailable
|
||||
}
|
||||
else if(throwableCause instanceof UnresolvedAddressException)
|
||||
{
|
||||
//Do nothing - the server is temporarily unavailable
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!mStackTraceLoggingSuppressed)
|
||||
HttpRequest updateRequest = createUpdateRequest(song);
|
||||
|
||||
if(updateRequest != null)
|
||||
{
|
||||
mLog.error("Error sending metadata update. Future errors will " +
|
||||
"be suppressed", e);
|
||||
|
||||
mStackTraceLoggingSuppressed = true;
|
||||
session.write(updateRequest);
|
||||
}
|
||||
|
||||
//Fetch next metadata update to send
|
||||
song = mMetadataQueue.poll();
|
||||
}
|
||||
|
||||
session.closeNow();
|
||||
}
|
||||
}
|
||||
}, 0l, TimeUnit.SECONDS);
|
||||
catch(UnresolvedAddressException | RuntimeIoException ure)
|
||||
{
|
||||
//Do nothing - the server is temporarily unavailable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(!mStackTraceLoggingSuppressed)
|
||||
{
|
||||
mLog.error("Error sending metadata update. Future errors will be suppressed", e);
|
||||
mStackTraceLoggingSuppressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
mUpdating.set(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Fetch next metadata update to send
|
||||
song = mMetadataQueue.poll();
|
||||
}
|
||||
|
||||
mUpdating.set(false);
|
||||
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,15 +27,14 @@ import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.core.write.WriteTimeoutException;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -46,7 +45,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
private final static String TERMINATOR = "\r\n";
|
||||
private final static String SEPARATOR = ":";
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
|
||||
private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds
|
||||
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
|
||||
private static final int WRITE_TIMEOUT_SECONDS = 5;
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
@@ -101,7 +101,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
|
||||
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
|
||||
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
@@ -127,45 +127,41 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
|
||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||
|
||||
mStreamingSession = future.getSession();
|
||||
if(connected)
|
||||
{
|
||||
mStreamingSession = future.getSession();
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(RuntimeIoException rioe)
|
||||
{
|
||||
if(rioe.getCause() instanceof SocketException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(UnresolvedAddressException uae)
|
||||
{
|
||||
//What do you do with this? Nothing.
|
||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
catch(InterruptedException ie)
|
||||
catch(Exception e)
|
||||
{
|
||||
//We couldn't connect within the timeout period -- disconnect so that we can try again later
|
||||
disconnect();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("[" + getStreamName() + "] failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
mLog.error("Error", e);
|
||||
//Disregard ... we'll disconnect and try again
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
mLog.error("Throwable error caught", t);
|
||||
}
|
||||
|
||||
disconnect();
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
@@ -188,6 +184,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -263,49 +260,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||
{
|
||||
if(cause instanceof IOException)
|
||||
{
|
||||
IOException ioe = (IOException)cause;
|
||||
|
||||
if(ioe.getMessage() != null)
|
||||
{
|
||||
String reason = ioe.getMessage();
|
||||
|
||||
if(connected())
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else if(reason.startsWith("Connection reset"))
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else if(ioe instanceof WriteTimeoutException)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
|
||||
}
|
||||
}
|
||||
else
|
||||
if(!(cause instanceof IOException))
|
||||
{
|
||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface IAudioConverter
|
||||
/**
|
||||
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
|
||||
*/
|
||||
public byte[] convert(List<AudioPacket> audioPackets);
|
||||
public byte[] convert(List<ReusableAudioPacket> audioPackets);
|
||||
|
||||
/**
|
||||
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.audio.AudioUtils;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import net.sourceforge.lame.lowlevel.LameEncoder;
|
||||
import net.sourceforge.lame.mp3.Lame;
|
||||
import net.sourceforge.lame.mp3.MPEGMode;
|
||||
@@ -54,7 +54,7 @@ public class MP3AudioConverter implements IAudioConverter
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(List<AudioPacket> audioPackets)
|
||||
public byte[] convert(List<ReusableAudioPacket> audioPackets)
|
||||
{
|
||||
mMP3Stream.reset();
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.record.mp3.MP3Recorder;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -33,6 +34,7 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
|
||||
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
|
||||
private byte[] mPreviousPartialFrameData;
|
||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator");
|
||||
|
||||
/**
|
||||
* Generates MP3 audio silence frames
|
||||
@@ -44,9 +46,10 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
public byte[] generate(long duration)
|
||||
{
|
||||
int length = (int)(duration * 8); //8000 Hz sample rate
|
||||
float[] silence = new float[length];
|
||||
AudioPacket silencePacket = new AudioPacket(silence, null);
|
||||
List<AudioPacket> silencePackets = new ArrayList<>();
|
||||
ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length);
|
||||
Arrays.fill(silencePacket.getAudioSamples(), 0.0f);
|
||||
|
||||
List<ReusableAudioPacket> silencePackets = new ArrayList<>();
|
||||
silencePackets.add(silencePacket);
|
||||
|
||||
byte[] frameData = mGenerator.convert(silencePackets);
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
public class AudioAdapter
|
||||
{
|
||||
private static int mGain = 5;
|
||||
private Oscillator mSineWaveGenerator;
|
||||
private IOscillator mSineWaveGenerator;
|
||||
private FloatFIRFilter mPostInversionFilter;
|
||||
private FloatFIRFilter mAudioHighPassFilter;
|
||||
private AudioType mAudioType = AudioType.NORMAL;
|
||||
@@ -88,7 +89,7 @@ public class AudioAdapter
|
||||
default:
|
||||
retVal = (float)( mPostInversionFilter.get(
|
||||
mAudioHighPassFilter.get( sample ) *
|
||||
mSineWaveGenerator.getFloat() ) );
|
||||
mSineWaveGenerator.inphase() ) );
|
||||
|
||||
mSineWaveGenerator.rotate();
|
||||
break;
|
||||
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
|
||||
import io.github.dsheirer.audio.InversionFrequency;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
@@ -29,7 +30,7 @@ import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
*/
|
||||
public class AudioInverter implements RealSampleListener
|
||||
{
|
||||
private Oscillator mSineWaveGenerator;
|
||||
private IOscillator mSineWaveGenerator;
|
||||
private FloatFIRFilter mPostInversionLowPassFilter;
|
||||
private RealSampleListener mListener;
|
||||
|
||||
@@ -61,7 +62,7 @@ public class AudioInverter implements RealSampleListener
|
||||
if( mPostInversionLowPassFilter != null )
|
||||
{
|
||||
mPostInversionLowPassFilter.receive(
|
||||
sample * mSineWaveGenerator.getFloat() );
|
||||
sample * mSineWaveGenerator.inphase() );
|
||||
|
||||
mSineWaveGenerator.rotate();
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
package io.github.dsheirer.audio.output;
|
||||
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
@@ -46,11 +46,11 @@ import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
public abstract class AudioOutput implements Listener<ReusableAudioPacket>, LineListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
|
||||
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
|
||||
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
|
||||
@@ -190,7 +190,7 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
* Converts the audio packet data into a byte buffer format appropriate for
|
||||
* the underlying source data line.
|
||||
*/
|
||||
protected abstract ByteBuffer convert(AudioPacket packet);
|
||||
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
|
||||
|
||||
/**
|
||||
* Audio output channel name
|
||||
@@ -271,7 +271,7 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket packet)
|
||||
public void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
if(mCanProcessAudio)
|
||||
{
|
||||
@@ -281,11 +281,17 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
|
||||
mBuffer.add(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
public class BufferProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private List<ReusableAudioPacket> mAudioPackets = new ArrayList<ReusableAudioPacket>();
|
||||
|
||||
|
||||
public BufferProcessor()
|
||||
{
|
||||
@@ -300,13 +306,11 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
* processor can run at any given time */
|
||||
if(mProcessing.compareAndSet(false, true))
|
||||
{
|
||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
||||
mBuffer.drainTo(mAudioPackets);
|
||||
|
||||
mBuffer.drainTo(packets);
|
||||
|
||||
for(AudioPacket packet : packets)
|
||||
for(ReusableAudioPacket packet : mAudioPackets)
|
||||
{
|
||||
if(packet.getType() == AudioPacket.Type.AUDIO)
|
||||
if(packet.getType() == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
broadcast(packet.getMetadata());
|
||||
|
||||
@@ -338,8 +342,14 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
|
||||
updateTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
mAudioPackets.clear();
|
||||
|
||||
checkStop();
|
||||
|
||||
mProcessing.set(false);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.output;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
@@ -43,14 +43,16 @@ public class MonoAudioOutput extends AudioOutput
|
||||
/**
|
||||
* Converts the audio packet data into mono audio frames.
|
||||
*/
|
||||
protected ByteBuffer convert(AudioPacket packet)
|
||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
||||
{
|
||||
if(packet.hasAudioBuffer())
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(packet.hasAudioSamples())
|
||||
{
|
||||
float[] samples = packet.getAudioBuffer().getSamples();
|
||||
float[] samples = packet.getAudioSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
@@ -58,10 +60,10 @@ public class MonoAudioOutput extends AudioOutput
|
||||
{
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
packet.decrementUserCount();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.output;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
@@ -45,15 +45,16 @@ public class StereoAudioOutput extends AudioOutput
|
||||
* channel containing the audio and the other channel containing zero
|
||||
* valued (silent) samples.
|
||||
*/
|
||||
protected ByteBuffer convert(AudioPacket packet)
|
||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
||||
{
|
||||
if(packet.hasAudioBuffer())
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(packet.hasAudioSamples())
|
||||
{
|
||||
float[] samples = packet.getAudioBuffer().getSamples();
|
||||
float[] samples = packet.getAudioSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 4)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
@@ -73,10 +74,10 @@ public class StereoAudioOutput extends AudioOutput
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
packet.decrementUserCount();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
|
||||
/**
|
||||
* Provides squelch control based on presense of decoded audio samples.
|
||||
*
|
||||
* NOTE: this class is not yet fully implemented with audio sample processing
|
||||
* and currently only provides user driven squelch control
|
||||
*/
|
||||
public class ManualSquelchController extends SquelchController
|
||||
{
|
||||
public ManualSquelchController()
|
||||
{
|
||||
mSquelchMode = SquelchMode.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchMode( SquelchMode mode )
|
||||
{
|
||||
//TODO: add functionality
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
|
||||
|
||||
/**
|
||||
* Squelch control - receives single audio samples or real audio buffers and
|
||||
* adjusts squelch state either based on the audio samples or based on external
|
||||
* control inputs.
|
||||
*
|
||||
* Assembles received single audio samples into real sample buffers for
|
||||
* consumption by an external audio processor, like the AudioPacketProcessor
|
||||
* class.
|
||||
*/
|
||||
public abstract class SquelchController
|
||||
implements Listener<RealBuffer>, RealSampleListener
|
||||
{
|
||||
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
|
||||
private Listener<RealBuffer> mAudioListener;
|
||||
protected SquelchState mSquelchState = SquelchState.UNSQUELCH;
|
||||
protected SquelchMode mSquelchMode = SquelchMode.MANUAL;
|
||||
|
||||
private RealToRealBufferAssembler mSampleAssembler = new RealToRealBufferAssembler( 512 );
|
||||
|
||||
public SquelchController()
|
||||
{
|
||||
mSampleAssembler.setListener( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
mSampleAssembler.receive( sample );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mAudioListener != null )
|
||||
{
|
||||
mAudioListener.receive( buffer );
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void setSquelchMode( SquelchMode mode );
|
||||
|
||||
public SquelchMode getSquelchMode()
|
||||
{
|
||||
return mSquelchMode;
|
||||
}
|
||||
|
||||
public void setSquelchState( SquelchState state )
|
||||
{
|
||||
mSquelchState = state;
|
||||
}
|
||||
|
||||
public SquelchState getSquelchState()
|
||||
{
|
||||
return mSquelchState;
|
||||
}
|
||||
|
||||
public boolean isSquelched()
|
||||
{
|
||||
switch( mSquelchMode )
|
||||
{
|
||||
case AUTOMATIC:
|
||||
case MANUAL:
|
||||
return mSquelchState == SquelchState.SQUELCH;
|
||||
case NONE:
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void addSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
if( listener != null )
|
||||
{
|
||||
mSquelchStateBroadcaster.addListener( listener );
|
||||
|
||||
/* Send current squelch state to the listener */
|
||||
listener.receive( mSquelchState );
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
mSquelchStateBroadcaster.removeListener( listener );
|
||||
}
|
||||
|
||||
public void setAudioListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAudioListener = listener;
|
||||
}
|
||||
|
||||
public void removeAudioListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAudioListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
public class TrunkingSquelchController extends SquelchController
|
||||
{
|
||||
private boolean mAllowSquelchOverride = true;
|
||||
|
||||
public TrunkingSquelchController()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides control over audio stream as directed by an external trunking
|
||||
* channel state.
|
||||
*
|
||||
* Override automatic squelch control by setting the squelch mode to none.
|
||||
*
|
||||
* Disable automatic control override by setting allowManualOverride to false;
|
||||
*
|
||||
* Note: this control does not respond to squelch mode manual.
|
||||
*/
|
||||
public TrunkingSquelchController( boolean allowSquelchOverride )
|
||||
{
|
||||
mAllowSquelchOverride = allowSquelchOverride;
|
||||
|
||||
mSquelchMode = SquelchMode.AUTOMATIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchMode( SquelchMode mode )
|
||||
{
|
||||
if( mode == SquelchMode.AUTOMATIC ||
|
||||
( mode == SquelchMode.NONE && mAllowSquelchOverride ) )
|
||||
{
|
||||
mSquelchMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.bits;
|
||||
|
||||
public interface IBinarySymbolProcessor
|
||||
{
|
||||
void receive(boolean symbol);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
public interface ISyncDetectListener
|
||||
{
|
||||
public void syncDetected();
|
||||
}
|
||||
@@ -1,6 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.bits;
|
||||
|
||||
public interface ISyncProcessor
|
||||
{
|
||||
public void checkSync( long value );
|
||||
/**
|
||||
* Checks if the value matches a known sync pattern
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
boolean checkSync(long value);
|
||||
}
|
||||
|
||||
@@ -1,119 +1,116 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.SyncDetectListener;
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.dsp.symbol.SyncDetectProvider;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MessageFramer - processes bitsets looking for a sync pattern within
|
||||
* the bits, and then extracts the message, including the sync
|
||||
* pattern, for a total bit length of messageLength.
|
||||
*
|
||||
*
|
||||
* Will extract multiple messages simultaneously, for each sync pattern that is
|
||||
* encountered within the bitset bit stream.
|
||||
*/
|
||||
public class MessageFramer implements Listener<Boolean>,
|
||||
SyncDetectProvider
|
||||
public class MessageFramer implements IBinarySymbolProcessor, Listener<Boolean>, SyncDetectProvider
|
||||
{
|
||||
private boolean[] mSyncPattern;
|
||||
private int mMessageLength;
|
||||
private SyncDetectListener mSyncDetectListener;
|
||||
private Broadcaster<BinaryMessage> mBroadcaster =
|
||||
new Broadcaster<BinaryMessage>();
|
||||
private ArrayList<MessageAssembler> mMessageAssemblers =
|
||||
new ArrayList<MessageAssembler>();
|
||||
private boolean[] mSyncPattern;
|
||||
private int mMessageLength;
|
||||
private ISyncDetectListener mSyncDetectListener;
|
||||
private Broadcaster<BinaryMessage> mBroadcaster = new Broadcaster<BinaryMessage>();
|
||||
private List<MessageAssembler> mMessageAssemblers = new ArrayList<MessageAssembler>();
|
||||
private List<MessageAssembler> mCompletedMessageAssemblers = new ArrayList<MessageAssembler>();
|
||||
private SyncPatternMatcher mMatcher;
|
||||
|
||||
private ArrayList<MessageAssembler> mCompletedMessageAssemblers =
|
||||
new ArrayList<MessageAssembler>();
|
||||
|
||||
private BinaryMessage mPreviousBuffer = null;
|
||||
|
||||
private SyncPatternMatcher mMatcher;
|
||||
|
||||
public MessageFramer( boolean[] syncPattern, int messageLength )
|
||||
public MessageFramer(boolean[] syncPattern, int messageLength)
|
||||
{
|
||||
mSyncPattern = syncPattern;
|
||||
mMatcher = new SyncPatternMatcher( syncPattern );
|
||||
mMatcher = new SyncPatternMatcher(syncPattern);
|
||||
mMessageLength = messageLength;
|
||||
}
|
||||
|
||||
|
||||
public void reset()
|
||||
{
|
||||
for( MessageAssembler assembler: mMessageAssemblers )
|
||||
{
|
||||
assembler.dispose();
|
||||
}
|
||||
|
||||
for(MessageAssembler assembler : mMessageAssemblers)
|
||||
{
|
||||
assembler.dispose();
|
||||
}
|
||||
|
||||
mMessageAssemblers.clear();
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mCompletedMessageAssemblers.clear();
|
||||
mMessageAssemblers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Boolean bit )
|
||||
public void dispose()
|
||||
{
|
||||
mMatcher.receive( bit );
|
||||
|
||||
mBroadcaster.dispose();
|
||||
mCompletedMessageAssemblers.clear();
|
||||
mMessageAssemblers.clear();
|
||||
}
|
||||
|
||||
public void receive(boolean bit)
|
||||
{
|
||||
mMatcher.receive(bit);
|
||||
|
||||
Iterator<MessageAssembler> it = mMessageAssemblers.iterator();
|
||||
|
||||
|
||||
MessageAssembler assembler;
|
||||
|
||||
while( it.hasNext() )
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
assembler = it.next();
|
||||
|
||||
/* Dispose and remove any completed assemblers */
|
||||
if( assembler.complete() )
|
||||
if(assembler.complete())
|
||||
{
|
||||
assembler.dispose();
|
||||
it.remove();
|
||||
assembler.dispose();
|
||||
it.remove();
|
||||
}
|
||||
/* Otherwise, send them the bit */
|
||||
else
|
||||
{
|
||||
assembler.receive( bit );
|
||||
assembler.receive(bit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Check for sync match and add new message assembler */
|
||||
if( mMatcher.matches() )
|
||||
{
|
||||
addMessageAssembler( new MessageAssembler( mMessageLength, mSyncPattern ) );
|
||||
|
||||
if(mMatcher.matches())
|
||||
{
|
||||
addMessageAssembler(new MessageAssembler(mMessageLength, mSyncPattern));
|
||||
|
||||
/* Notify any sync detect listener(s) */
|
||||
if( mSyncDetectListener != null )
|
||||
if(mSyncDetectListener != null)
|
||||
{
|
||||
mSyncDetectListener.syncDetected();
|
||||
mSyncDetectListener.syncDetected();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Deprecated //Legacy support ... remove once all producers are converted to use receive(boolean bit) method
|
||||
@Override
|
||||
public void receive(Boolean bit)
|
||||
{
|
||||
receive(bit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes all messages currently under assembly to be forcibly
|
||||
* sent (ie flushed) to all registered message listeners, and
|
||||
@@ -121,44 +118,44 @@ public class MessageFramer implements Listener<Boolean>,
|
||||
*/
|
||||
public void flush()
|
||||
{
|
||||
for( MessageAssembler assembler: mMessageAssemblers )
|
||||
for(MessageAssembler assembler : mMessageAssemblers)
|
||||
{
|
||||
assembler.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSyncDetectListener( SyncDetectListener listener )
|
||||
|
||||
@Override
|
||||
public void setSyncDetectListener(ISyncDetectListener listener)
|
||||
{
|
||||
mSyncDetectListener = listener;
|
||||
mSyncDetectListener = listener;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allow a message listener to register with this framer to receive
|
||||
* all framed messages
|
||||
*/
|
||||
public void addMessageListener( Listener<BinaryMessage> listener )
|
||||
public void addMessageListener(Listener<BinaryMessage> listener)
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
mBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeMessageListener( Listener<BinaryMessage> listener )
|
||||
|
||||
public void removeMessageListener(Listener<BinaryMessage> listener)
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
mBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds a message assembler to receive bits from the bit stream
|
||||
*/
|
||||
private void addMessageAssembler( MessageAssembler assembler )
|
||||
private void addMessageAssembler(MessageAssembler assembler)
|
||||
{
|
||||
mMessageAssemblers.add( assembler );
|
||||
mMessageAssemblers.add(assembler);
|
||||
}
|
||||
|
||||
@SuppressWarnings( "unused" )
|
||||
private void removeMessageAssembler( MessageAssembler assembler )
|
||||
@SuppressWarnings("unused")
|
||||
private void removeMessageAssembler(MessageAssembler assembler)
|
||||
{
|
||||
mMessageAssemblers.remove( assembler );
|
||||
mMessageAssemblers.remove(assembler);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +163,7 @@ public class MessageFramer implements Listener<Boolean>,
|
||||
* identified sync pattern, and every bit thereafter. Once the accumulated
|
||||
* bits equal the message length, the message is sent and the assembler
|
||||
* flags itself as complete.
|
||||
*
|
||||
*
|
||||
* By design, multiple message assemblers can exist at the same time, each
|
||||
* assembling different, overlapping potential messages
|
||||
*/
|
||||
@@ -174,46 +171,46 @@ public class MessageFramer implements Listener<Boolean>,
|
||||
{
|
||||
BinaryMessage mMessage;
|
||||
boolean mComplete = false;
|
||||
|
||||
MessageAssembler( int messageLength )
|
||||
|
||||
MessageAssembler(int messageLength)
|
||||
{
|
||||
mMessage = new BinaryMessage( messageLength );
|
||||
mMessage = new BinaryMessage(messageLength);
|
||||
}
|
||||
|
||||
MessageAssembler( int messageLength, boolean[] initialFill )
|
||||
|
||||
MessageAssembler(int messageLength, boolean[] initialFill)
|
||||
{
|
||||
this( messageLength );
|
||||
|
||||
this(messageLength);
|
||||
|
||||
/* Pre-load the message with the sync pattern */
|
||||
for( int x = 0; x < initialFill.length; x++ )
|
||||
for(int x = 0; x < initialFill.length; x++)
|
||||
{
|
||||
receive( initialFill[ x ] );
|
||||
receive(initialFill[x]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mMessage = null;
|
||||
mMessage = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Receives one bit at a time, and assembles them into a message
|
||||
*/
|
||||
public void receive( Boolean bit )
|
||||
public void receive(Boolean bit)
|
||||
{
|
||||
try
|
||||
{
|
||||
mMessage.add( bit );
|
||||
mMessage.add(bit);
|
||||
}
|
||||
catch( BitSetFullException e )
|
||||
catch(BitSetFullException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
/* Once our message is complete (ie full), send it to all registered
|
||||
* message listeners, and set complete flag so for auto-removal */
|
||||
if( mMessage.isFull() )
|
||||
if(mMessage.isFull())
|
||||
{
|
||||
mComplete = true;
|
||||
flush();
|
||||
@@ -226,7 +223,7 @@ public class MessageFramer implements Listener<Boolean>,
|
||||
*/
|
||||
public void flush()
|
||||
{
|
||||
mBroadcaster.receive( mMessage );
|
||||
mBroadcaster.receive(mMessage);
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +1,130 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Multi-Sync pattern matcher. Accepts multiple sync detector objects for
|
||||
* Multi-Sync pattern matcher. Accepts multiple sync detector objects for
|
||||
* comparision against the incoming bit stream.
|
||||
*
|
||||
*
|
||||
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
|
||||
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
|
||||
* of the Long.rotate methods will wrap the MSB around to the LSB and
|
||||
* corrupt the matching value.
|
||||
*
|
||||
*/
|
||||
public class MultiSyncPatternMatcher
|
||||
{
|
||||
private List<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
|
||||
|
||||
private long mBits = 0;
|
||||
private long mMask = 0;
|
||||
|
||||
public MultiSyncPatternMatcher( int syncSize )
|
||||
{
|
||||
Validate.isTrue(syncSize < 64);
|
||||
|
||||
//Setup a bit mask of all ones the length of the sync pattern
|
||||
mMask = (long)( ( Math.pow( 2, syncSize ) ) - 1 );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mSyncProcessors.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes two bits before checking sync processors for a match.
|
||||
*/
|
||||
public void receive( boolean bit1, boolean bit2 )
|
||||
{
|
||||
mBits = Long.rotateLeft( mBits, 1 );
|
||||
|
||||
mBits &= mMask;
|
||||
|
||||
if( bit1 )
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);
|
||||
|
||||
mBits = Long.rotateLeft( mBits, 1 );
|
||||
|
||||
mBits &= mMask;
|
||||
|
||||
if( bit2 )
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
private ISyncDetectListener mSyncDetectListener;
|
||||
private int mSyncLossThreshold;
|
||||
private int mBitCount;
|
||||
private List<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
|
||||
|
||||
for( ISyncProcessor processor: mSyncProcessors )
|
||||
{
|
||||
processor.checkSync( mBits );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes one bit before checking sync processors for a match.
|
||||
*/
|
||||
public void receive( boolean bit )
|
||||
{
|
||||
mBits = Long.rotateLeft( mBits, 1 );
|
||||
|
||||
mBits &= mMask;
|
||||
|
||||
if( bit )
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
private long mBits = 0;
|
||||
private long mMask = 0;
|
||||
|
||||
for( ISyncProcessor processor: mSyncProcessors )
|
||||
{
|
||||
processor.checkSync( mBits );
|
||||
}
|
||||
}
|
||||
public MultiSyncPatternMatcher(ISyncDetectListener syncDetectListener, int syncLossThreshold, int syncSize)
|
||||
{
|
||||
mSyncDetectListener = syncDetectListener;
|
||||
mSyncLossThreshold = syncLossThreshold;
|
||||
Validate.isTrue(syncSize < 64);
|
||||
|
||||
/**
|
||||
* Adds a sync processor to receive the bit stream.
|
||||
*/
|
||||
public void add( ISyncProcessor processor )
|
||||
{
|
||||
mSyncProcessors.add( processor );
|
||||
}
|
||||
//Setup a bit mask of all ones the length of the sync pattern
|
||||
mMask = (long)((Math.pow(2, syncSize)) - 1);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mSyncProcessors.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes two bits before checking sync processors for a match.
|
||||
*/
|
||||
public void receive(boolean bit1, boolean bit2)
|
||||
{
|
||||
mBits = Long.rotateLeft(mBits, 1);
|
||||
|
||||
mBits &= mMask;
|
||||
|
||||
if(bit1)
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
|
||||
mBits = Long.rotateLeft(mBits, 1);
|
||||
|
||||
mBits &= mMask;
|
||||
|
||||
if(bit2)
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
|
||||
mBitCount += 2;
|
||||
|
||||
for(ISyncProcessor processor : mSyncProcessors)
|
||||
{
|
||||
if(processor.checkSync(mBits))
|
||||
{
|
||||
mSyncDetectListener.syncDetected();
|
||||
mBitCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(mBitCount > mSyncLossThreshold)
|
||||
{
|
||||
mBitCount = 0;
|
||||
mSyncDetectListener.syncLost();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes one bit before checking sync processors for a match.
|
||||
*/
|
||||
public void receive(boolean bit)
|
||||
{
|
||||
mBits = Long.rotateLeft(mBits, 1);
|
||||
|
||||
mBits &= mMask;
|
||||
|
||||
if(bit)
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
|
||||
for(ISyncProcessor processor : mSyncProcessors)
|
||||
{
|
||||
processor.checkSync(mBits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a sync processor to receive the bit stream.
|
||||
*/
|
||||
public void add(ISyncProcessor processor)
|
||||
{
|
||||
mSyncProcessors.add(processor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,68 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
|
||||
public class SoftSyncDetector implements ISyncProcessor
|
||||
{
|
||||
private ISyncDetectListener mListener;
|
||||
private long mPattern;
|
||||
private int mThreshold;
|
||||
|
||||
public SoftSyncDetector( ISyncDetectListener listener, long pattern, int threshold )
|
||||
{
|
||||
this( pattern, threshold );
|
||||
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public SoftSyncDetector( long pattern, int threshold )
|
||||
{
|
||||
mPattern = pattern;
|
||||
mThreshold = threshold;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
private ISyncDetectListener mListener;
|
||||
private long mPattern;
|
||||
private int mThreshold;
|
||||
|
||||
@Override
|
||||
public void checkSync( long value )
|
||||
{
|
||||
long difference = value ^ mPattern;
|
||||
|
||||
if( ( difference == 0 || Long.bitCount( difference ) <= mThreshold ) &&
|
||||
mListener != null )
|
||||
{
|
||||
mListener.syncDetected();
|
||||
}
|
||||
}
|
||||
|
||||
public void setThreshold( int threshold )
|
||||
{
|
||||
mThreshold = threshold;
|
||||
}
|
||||
|
||||
public void setListener( ISyncDetectListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
public SoftSyncDetector(ISyncDetectListener listener, long pattern, int threshold)
|
||||
{
|
||||
this(pattern, threshold);
|
||||
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public SoftSyncDetector(long pattern, int threshold)
|
||||
{
|
||||
mPattern = pattern;
|
||||
mThreshold = threshold;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkSync(long value)
|
||||
{
|
||||
long difference = value ^ mPattern;
|
||||
|
||||
if((difference == 0 || Long.bitCount(difference) <= mThreshold) &&
|
||||
mListener != null)
|
||||
{
|
||||
mListener.syncDetected();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setThreshold(int threshold)
|
||||
{
|
||||
mThreshold = threshold;
|
||||
}
|
||||
|
||||
public void setListener(ISyncDetectListener listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,57 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
|
||||
public class SyncDetector implements ISyncProcessor
|
||||
{
|
||||
private long mPattern;
|
||||
private ISyncDetectListener mListener;
|
||||
private long mPattern;
|
||||
private ISyncDetectListener mListener;
|
||||
|
||||
public SyncDetector( long pattern )
|
||||
{
|
||||
mPattern = pattern;
|
||||
}
|
||||
|
||||
public SyncDetector( long pattern, ISyncDetectListener listener )
|
||||
{
|
||||
this( pattern );
|
||||
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void setListener( ISyncDetectListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
public SyncDetector(long pattern)
|
||||
{
|
||||
mPattern = pattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkSync( long value )
|
||||
{
|
||||
if( value == mPattern && mListener != null )
|
||||
{
|
||||
mListener.syncDetected();
|
||||
}
|
||||
}
|
||||
public SyncDetector(long pattern, ISyncDetectListener listener)
|
||||
{
|
||||
this(pattern);
|
||||
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void setListener(ISyncDetectListener listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkSync(long value)
|
||||
{
|
||||
if(value == mPattern)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.syncDetected();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public enum SyncPattern
|
||||
false, false, false, false //0000 0x0
|
||||
} ),
|
||||
|
||||
/* Revs (0x0A) and Sync (0x23EB) = 10101 0010001111101011 */
|
||||
/* Revs (0x0A) and Sync (0x23EB) = 0101 000 1 000 11111 0 1 0 11 */
|
||||
FLEETSYNC2( new boolean[]
|
||||
{
|
||||
false, true, false, true, false, //End of bit revs
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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/>
|
||||
******************************************************************************/
|
||||
@@ -20,113 +20,112 @@ package io.github.dsheirer.bits;
|
||||
|
||||
/**
|
||||
* Sync pattern matcher.
|
||||
*
|
||||
*
|
||||
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
|
||||
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
|
||||
* of the Long.rotate methods will wrap the MSB around to the LSB and
|
||||
* corrupt the matching value.
|
||||
*
|
||||
*/
|
||||
public class SyncPatternMatcher
|
||||
{
|
||||
private long mBits = 0;
|
||||
private long mMask = 0;
|
||||
private long mSync = 0;
|
||||
private boolean mSoftMode = false;
|
||||
private int mSoftModeErrorThreshold = 0;
|
||||
|
||||
public SyncPatternMatcher( boolean[] syncPattern )
|
||||
{
|
||||
//Setup a bit mask of all ones the length of the sync pattern
|
||||
mMask = (long)( ( Math.pow( 2, syncPattern.length ) ) - 1 );
|
||||
|
||||
//Convert the sync bits into a long value for comparison
|
||||
for( int x = 0; x < syncPattern.length; x++ )
|
||||
{
|
||||
if( syncPattern[ x ] )
|
||||
{
|
||||
mSync += 1l << ( syncPattern.length - 1 - x );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SyncPatternMatcher( long sync )
|
||||
{
|
||||
mSync = sync;
|
||||
mMask = getMask( sync );
|
||||
}
|
||||
private long mBits = 0;
|
||||
private long mMask = 0;
|
||||
private long mSync = 0;
|
||||
private boolean mSoftMode = false;
|
||||
private int mSoftModeErrorThreshold = 0;
|
||||
|
||||
/**
|
||||
* Constructs the sync pattern matcher and defines the soft mode maximum
|
||||
* number of bit errors to allow in the pattern match.
|
||||
*
|
||||
* @param sync pattern
|
||||
* @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
|
||||
* positions) when considering a sync match
|
||||
*/
|
||||
public SyncPatternMatcher( long sync, int softModeErrorThreshold )
|
||||
{
|
||||
this( sync );
|
||||
|
||||
mSoftModeErrorThreshold = softModeErrorThreshold;
|
||||
}
|
||||
public SyncPatternMatcher(boolean[] syncPattern)
|
||||
{
|
||||
//Setup a bit mask of all ones the length of the sync pattern
|
||||
mMask = (long)((Math.pow(2, syncPattern.length)) - 1);
|
||||
|
||||
/**
|
||||
* Generates an all ones mask corresponding to the sync pattern
|
||||
*/
|
||||
private static long getMask( long sync )
|
||||
{
|
||||
long mask = sync;
|
||||
|
||||
for( int x = 1; x < 64; x *= 2 )
|
||||
{
|
||||
mask |= ( mask >> x );
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
|
||||
public void receive( boolean bit )
|
||||
{
|
||||
//Left shift the previous value
|
||||
mBits = Long.rotateLeft( mBits, 1 );
|
||||
|
||||
//Apply the mask to erase the previous MSB
|
||||
mBits &= mMask;
|
||||
|
||||
//Add in the new bit
|
||||
if( bit )
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
}
|
||||
//Convert the sync bits into a long value for comparison
|
||||
for(int x = 0; x < syncPattern.length; x++)
|
||||
{
|
||||
if(syncPattern[x])
|
||||
{
|
||||
mSync += 1l << (syncPattern.length - 1 - x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the most recently received bit sequence matches the
|
||||
* sync pattern
|
||||
*/
|
||||
public boolean matches()
|
||||
{
|
||||
if( mSoftMode )
|
||||
{
|
||||
long difference = mBits ^ mSync;
|
||||
|
||||
return difference == 0 ||
|
||||
Long.bitCount( difference ) <= mSoftModeErrorThreshold;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ( mBits == mSync );
|
||||
}
|
||||
}
|
||||
public SyncPatternMatcher(long sync)
|
||||
{
|
||||
mSync = sync;
|
||||
mMask = getMask(sync);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sync pattern matcher in soft mode which allows up to the soft
|
||||
* mode threshold number of bit errors when considering a sync pattern match
|
||||
*/
|
||||
public void setSoftMode( boolean softMode )
|
||||
{
|
||||
mSoftMode = softMode;
|
||||
}
|
||||
/**
|
||||
* Constructs the sync pattern matcher and defines the soft mode maximum
|
||||
* number of bit errors to allow in the pattern match.
|
||||
*
|
||||
* @param sync pattern
|
||||
* @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
|
||||
* positions) when considering a sync match
|
||||
*/
|
||||
public SyncPatternMatcher(long sync, int softModeErrorThreshold)
|
||||
{
|
||||
this(sync);
|
||||
|
||||
mSoftModeErrorThreshold = softModeErrorThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an all ones mask corresponding to the sync pattern
|
||||
*/
|
||||
private static long getMask(long sync)
|
||||
{
|
||||
long mask = sync;
|
||||
|
||||
for(int x = 1; x < 64; x *= 2)
|
||||
{
|
||||
mask |= (mask >> x);
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
|
||||
public void receive(boolean bit)
|
||||
{
|
||||
//Left shift the previous value
|
||||
mBits = Long.rotateLeft(mBits, 1);
|
||||
|
||||
//Apply the mask to erase the previous MSB
|
||||
mBits &= mMask;
|
||||
|
||||
//Add in the new bit
|
||||
if(bit)
|
||||
{
|
||||
mBits += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the most recently received bit sequence matches the
|
||||
* sync pattern
|
||||
*/
|
||||
public boolean matches()
|
||||
{
|
||||
if(mSoftMode)
|
||||
{
|
||||
long difference = mBits ^ mSync;
|
||||
|
||||
return difference == 0 ||
|
||||
Long.bitCount(difference) <= mSoftModeErrorThreshold;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (mBits == mSync);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sync pattern matcher in soft mode which allows up to the soft
|
||||
* mode threshold number of bit errors when considering a sync pattern match
|
||||
*/
|
||||
public void setSoftMode(boolean softMode)
|
||||
{
|
||||
mSoftMode = softMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,89 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.buffer;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
/**
|
||||
* Circular Buffer - implements a circular buffer with a method to get the
|
||||
* current head of the buffer, and put a new value in its place at the same
|
||||
* time
|
||||
*
|
||||
*
|
||||
* Also provides averaging over the elements in the buffer
|
||||
*/
|
||||
public class BooleanAveragingBuffer {
|
||||
public class BooleanAveragingBuffer
|
||||
{
|
||||
private boolean[] mBuffer;
|
||||
private int mBufferPointer;
|
||||
private int mThreshold;
|
||||
private int mTrueCount;
|
||||
|
||||
private boolean[] mBuffer;
|
||||
private int mBufferPointer;
|
||||
|
||||
public BooleanAveragingBuffer( int length )
|
||||
{
|
||||
mBuffer = new boolean[ length ];
|
||||
|
||||
//Preload the array with false values
|
||||
Arrays.fill( mBuffer, false );
|
||||
}
|
||||
|
||||
public boolean get( boolean newValue )
|
||||
{
|
||||
//Fetch current boolean value
|
||||
boolean retVal = mBuffer[ mBufferPointer ];
|
||||
public BooleanAveragingBuffer(int length)
|
||||
{
|
||||
mBuffer = new boolean[length];
|
||||
mThreshold = length / 2;
|
||||
}
|
||||
|
||||
//Load the new value into the buffer
|
||||
put( newValue );
|
||||
|
||||
return retVal;
|
||||
}
|
||||
public boolean get(boolean newValue)
|
||||
{
|
||||
//Fetch current boolean value
|
||||
boolean retVal = mBuffer[mBufferPointer];
|
||||
|
||||
/**
|
||||
* Loads the newValue into this buffer and adjusts the buffer pointer
|
||||
* to prepare for the next get/put cycle
|
||||
*/
|
||||
private void put( boolean newValue )
|
||||
{
|
||||
//Store the new value to the buffer
|
||||
mBuffer[ mBufferPointer ] = newValue;
|
||||
|
||||
//Increment the buffer pointer
|
||||
mBufferPointer++;
|
||||
//Load the new value into the buffer
|
||||
put(newValue);
|
||||
|
||||
//Wrap the buffer pointer around to 0 when necessary
|
||||
if( mBufferPointer >= mBuffer.length )
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the newValue into the buffer, calculates the average
|
||||
* and returns that average from this method
|
||||
*
|
||||
* This effectively performs low-pass filtering
|
||||
*/
|
||||
public boolean getAverage( boolean newValue )
|
||||
{
|
||||
//Load the new value into the buffer
|
||||
put( newValue );
|
||||
|
||||
int trueCount = 0;
|
||||
|
||||
for( int x = 0; x < mBuffer.length; x++ )
|
||||
{
|
||||
if( mBuffer[ x ] )
|
||||
{
|
||||
trueCount++;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loads the newValue into this buffer and adjusts the buffer pointer
|
||||
* to prepare for the next get/put cycle
|
||||
*/
|
||||
private void put(boolean newValue)
|
||||
{
|
||||
//Store the new value to the buffer
|
||||
mBuffer[mBufferPointer] = newValue;
|
||||
|
||||
/**
|
||||
* If the number of true values in the buffer is
|
||||
* more than half, return true, otherwise, false
|
||||
*/
|
||||
if( trueCount > (int)( mBuffer.length / 2 ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//Increment the buffer pointer
|
||||
mBufferPointer++;
|
||||
|
||||
mBufferPointer = mBufferPointer % mBuffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the newValue into the buffer, calculates the average
|
||||
* and returns that average from this method
|
||||
*
|
||||
* This effectively performs low-pass filtering
|
||||
*/
|
||||
public boolean getAverage(boolean newValue)
|
||||
{
|
||||
//Load the new value into the buffer
|
||||
put(newValue);
|
||||
|
||||
mTrueCount = 0;
|
||||
|
||||
for(int x = 0; x < mBuffer.length; x++)
|
||||
{
|
||||
if(mBuffer[x])
|
||||
{
|
||||
mTrueCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return mTrueCount > mThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.buffer;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -8,87 +23,86 @@ import java.util.List;
|
||||
|
||||
public class CircularBuffer<T> implements Listener<T>
|
||||
{
|
||||
private ArrayList<T> mElements;
|
||||
private Listener<T> mListener;
|
||||
private int mPointer;
|
||||
private int mSize;
|
||||
private List<T> mElements;
|
||||
private Listener<T> mListener;
|
||||
private int mPointer;
|
||||
private int mSize;
|
||||
|
||||
|
||||
/**
|
||||
* Circular Buffer - stores elements in an array list. Once the buffer is
|
||||
* full, new samples replace older samples and older samples are broadcast
|
||||
* to an optional listener.
|
||||
*
|
||||
* @param size - size of buffer
|
||||
*/
|
||||
public CircularBuffer( int size )
|
||||
{
|
||||
mSize = size;
|
||||
mElements = new ArrayList<T>();
|
||||
}
|
||||
/**
|
||||
* Circular Buffer - stores elements in an array list. Once the buffer is
|
||||
* full, new samples replace older samples and older samples are broadcast
|
||||
* to an optional listener.
|
||||
*
|
||||
* @param size - size of buffer
|
||||
*/
|
||||
public CircularBuffer(int size)
|
||||
{
|
||||
mSize = size;
|
||||
mElements = new ArrayList<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element stored at index. Note: the index argument refers to
|
||||
* the internal storage index and does not indicate a temporal relationship
|
||||
* among elements. For example, if you get indexes 1, 2, and 3, you may be
|
||||
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
|
||||
* is currently pointing to and will insert the next element at index 4.
|
||||
*
|
||||
* @param index - element index
|
||||
*
|
||||
* @return element or null
|
||||
*/
|
||||
public T get( int index )
|
||||
{
|
||||
if( index < mElements.size() )
|
||||
{
|
||||
return mElements.get( index );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mSize;
|
||||
}
|
||||
|
||||
public List<T> getElements()
|
||||
{
|
||||
return Collections.unmodifiableList( mElements );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( T element )
|
||||
{
|
||||
T previous = null;
|
||||
|
||||
if( mElements.size() > mPointer )
|
||||
{
|
||||
previous = mElements.get( mPointer );
|
||||
|
||||
mElements.set( mPointer, element );
|
||||
}
|
||||
else
|
||||
{
|
||||
mElements.add( element );
|
||||
}
|
||||
|
||||
mPointer++;
|
||||
|
||||
if( mPointer >= mSize )
|
||||
{
|
||||
mPointer = 0;
|
||||
}
|
||||
|
||||
if( previous != null && mListener != null )
|
||||
{
|
||||
mListener.receive( previous );
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the element stored at index. Note: the index argument refers to
|
||||
* the internal storage index and does not indicate a temporal relationship
|
||||
* among elements. For example, if you get indexes 1, 2, and 3, you may be
|
||||
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
|
||||
* is currently pointing to and will insert the next element at index 4.
|
||||
*
|
||||
* @param index - element index
|
||||
* @return element or null
|
||||
*/
|
||||
public T get(int index)
|
||||
{
|
||||
if(index < mElements.size())
|
||||
{
|
||||
return mElements.get(index);
|
||||
}
|
||||
|
||||
public void setListener( Listener<T> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mSize;
|
||||
}
|
||||
|
||||
public List<T> getElements()
|
||||
{
|
||||
return Collections.unmodifiableList(mElements);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(T element)
|
||||
{
|
||||
T previous = null;
|
||||
|
||||
if(mElements.size() > mPointer)
|
||||
{
|
||||
previous = mElements.get(mPointer);
|
||||
|
||||
mElements.set(mPointer, element);
|
||||
}
|
||||
else
|
||||
{
|
||||
mElements.add(element);
|
||||
}
|
||||
|
||||
mPointer++;
|
||||
|
||||
if(mPointer >= mSize)
|
||||
{
|
||||
mPointer = 0;
|
||||
}
|
||||
|
||||
if(previous != null && mListener != null)
|
||||
{
|
||||
mListener.receive(previous);
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(Listener<T> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
124
src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java
Normal file
124
src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.buffer;
|
||||
|
||||
/**
|
||||
* Circular sample buffer - allocates a buffer and stores samples in a circular
|
||||
* fashion, overwriting older samples with newly arrived samples.
|
||||
*
|
||||
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
|
||||
*/
|
||||
public class RealCircularBuffer
|
||||
{
|
||||
float[] mBuffer;
|
||||
int mBufferPointer = 0;
|
||||
|
||||
public RealCircularBuffer(int size)
|
||||
{
|
||||
mBuffer = new float[size];
|
||||
}
|
||||
|
||||
public int getSize()
|
||||
{
|
||||
return mBuffer.length;
|
||||
}
|
||||
|
||||
public float[] getBuffer()
|
||||
{
|
||||
return mBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the new value into the buffer and returns the oldest buffer value
|
||||
* that it replaced
|
||||
*
|
||||
* @param newValue
|
||||
* @return
|
||||
*/
|
||||
public float putAndGet(float newValue)
|
||||
{
|
||||
float oldestSample = mBuffer[mBufferPointer];
|
||||
|
||||
put(newValue);
|
||||
|
||||
return oldestSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the new value to this buffer.
|
||||
*/
|
||||
public void put(float newValue)
|
||||
{
|
||||
mBuffer[mBufferPointer] = newValue;
|
||||
|
||||
mBufferPointer++;
|
||||
|
||||
if(mBufferPointer >= mBuffer.length)
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the
|
||||
* newest sample.
|
||||
* @param index in range of 0 to length-1
|
||||
* @return value at indexed position
|
||||
*/
|
||||
|
||||
public float get(int index)
|
||||
{
|
||||
if(index < mBuffer.length)
|
||||
{
|
||||
int pointer = mBufferPointer + index;
|
||||
|
||||
if(pointer >= mBuffer.length)
|
||||
{
|
||||
pointer -= mBuffer.length;
|
||||
}
|
||||
|
||||
return mBuffer[pointer];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" +
|
||||
mBuffer.length + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1
|
||||
* is the newest sample.
|
||||
*
|
||||
* @return array of samples
|
||||
*/
|
||||
public float[] get()
|
||||
{
|
||||
float[] samples = new float[mBuffer.length];
|
||||
|
||||
System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer);
|
||||
|
||||
if(mBufferPointer != 0)
|
||||
{
|
||||
System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,10 @@ import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.module.decode.event.CallEventPanel;
|
||||
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.spectrum.ChannelSpectrumPanel;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
|
||||
public class ChannelMetadataViewer extends JPanel
|
||||
{
|
||||
@@ -39,7 +38,6 @@ public class ChannelMetadataViewer extends JPanel
|
||||
private ChannelDetailPanel mChannelDetailPanel;
|
||||
private CallEventPanel mCallEventPanel;
|
||||
private MessageActivityPanel mMessageActivityPanel;
|
||||
private ChannelSpectrumPanel mChannelSpectrumPanel;
|
||||
|
||||
/**
|
||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||
@@ -51,7 +49,6 @@ public class ChannelMetadataViewer extends JPanel
|
||||
mChannelDetailPanel = new ChannelDetailPanel(channelProcessingManager);
|
||||
mCallEventPanel = new CallEventPanel(iconManager);
|
||||
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
|
||||
// mChannelSpectrumPanel = new ChannelSpectrumPanel(settingsManager, channelProcessingManager);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(channelProcessingManager, iconManager);
|
||||
|
||||
init();
|
||||
|
||||
@@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Basic decoder channel state - provides the minimum channel state functionality
|
||||
* to support an always un-squelched audio decoder.
|
||||
@@ -94,7 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
||||
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
|
||||
|
||||
@@ -20,8 +20,6 @@ package io.github.dsheirer.channel.state;
|
||||
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.channel.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.channel.heartbeat.IHeartbeatListener;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener;
|
||||
@@ -34,20 +32,19 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.module.decode.event.ICallEventProvider;
|
||||
import io.github.dsheirer.sample.IOverflowListener;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IOverflowListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import static io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
|
||||
|
||||
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
|
||||
IDecoderStateEventProvider, IFrequencyChangeListener, IHeartbeatListener, ISquelchStateProvider,
|
||||
IAttributeChangeRequestListener, IOverflowListener
|
||||
IDecoderStateEventProvider, ISourceEventListener, ISourceEventProvider, IHeartbeatListener,
|
||||
ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
|
||||
|
||||
@@ -59,12 +56,13 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
private Listener<CallEvent> mCallEventListener;
|
||||
private Listener<DecoderStateEvent> mDecoderStateListener;
|
||||
private Listener<SquelchState> mSquelchStateListener;
|
||||
private Listener<SourceEvent> mExternalSourceEventListener;
|
||||
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
||||
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
||||
private ChannelType mChannelType;
|
||||
private TrafficChannelManager mTrafficChannelEndListener;
|
||||
private CallEvent mTrafficChannelCallEvent;
|
||||
private FrequencyChangeListener mFrequencyChangeListener;
|
||||
private SourceEventListener mInternalSourceEventListener;
|
||||
|
||||
private boolean mSquelchLocked = false;
|
||||
private boolean mSelected = false;
|
||||
@@ -124,7 +122,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, mState));
|
||||
|
||||
@@ -160,14 +158,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
if(mFrequencyChangeListener == null)
|
||||
if(mInternalSourceEventListener == null)
|
||||
{
|
||||
mFrequencyChangeListener = new FrequencyChangeListener();
|
||||
mInternalSourceEventListener = new SourceEventListener();
|
||||
}
|
||||
|
||||
return mFrequencyChangeListener;
|
||||
return mInternalSourceEventListener;
|
||||
}
|
||||
|
||||
private boolean isStandardChannel()
|
||||
@@ -257,6 +255,17 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the source event to a registered external source event listener
|
||||
*/
|
||||
protected void broadcast(SourceEvent sourceEvent)
|
||||
{
|
||||
if(mExternalSourceEventListener != null)
|
||||
{
|
||||
mExternalSourceEventListener.receive(sourceEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the squelch state listener
|
||||
*/
|
||||
@@ -518,18 +527,46 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to receive frequency change events and rebroadcast them to the decoder states
|
||||
* Registers the listener to receive source events from the channel state
|
||||
*/
|
||||
public class FrequencyChangeListener implements Listener<FrequencyChangeEvent>
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mExternalSourceEventListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-Registers a listener from receiving source events from the channel state
|
||||
*/
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mExternalSourceEventListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to receive source events.
|
||||
*/
|
||||
public class SourceEventListener implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent frequencyChangeEvent)
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
long frequency = frequencyChangeEvent.getValue().longValue();
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Rebroadcast source frequency change events for the decoder(s) to process
|
||||
long frequency = sourceEvent.getValue().longValue();
|
||||
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
|
||||
break;
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||
//Rebroadcast frequency error measurements to external listener if we're currently
|
||||
//in an active (ie sync locked) state.
|
||||
if(getState().isActiveState())
|
||||
{
|
||||
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), mChannelType.name()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,17 @@ public enum State
|
||||
|
||||
public abstract boolean canChangeTo(State state);
|
||||
|
||||
/**
|
||||
* Indicates that this state is an active (ie decoding) state and is one of the states
|
||||
* enumerated in the CALL_STATES enumeration set.
|
||||
*
|
||||
* @return true if this state is an active state
|
||||
*/
|
||||
public boolean isActiveState()
|
||||
{
|
||||
return CALL_STATES.contains(this);
|
||||
}
|
||||
|
||||
public String getDisplayValue()
|
||||
{
|
||||
return mDisplayValue;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 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 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.
|
||||
* 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/>
|
||||
* 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.channel.traffic;
|
||||
@@ -29,13 +26,12 @@ import io.github.dsheirer.controller.channel.TrafficChannelEvent;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent.CallEventType;
|
||||
import io.github.dsheirer.module.decode.event.ICallEventProvider;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel.Type;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -43,7 +39,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener
|
||||
{
|
||||
@@ -61,7 +56,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
|
||||
private ChannelModel mChannelModel;
|
||||
private DecodeConfiguration mDecodeConfiguration;
|
||||
private EventLogConfiguration mEventLogConfiguration;
|
||||
private RecordConfiguration mRecordConfiguration;
|
||||
private String mSystem;
|
||||
private String mSite;
|
||||
@@ -75,7 +69,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
* @param channelModel containing channels currently in use
|
||||
* @param decodeConfiguration - decoder configuration to use for each
|
||||
* traffic channel allocation.
|
||||
* @param eventLogConfiguration - logging configuration to use for traffic channels
|
||||
* @param recordConfiguration - recording options for each traffic channel
|
||||
* @param system label to use to describe the system
|
||||
* @param site label to use to describe the site
|
||||
@@ -85,7 +78,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
*/
|
||||
public TrafficChannelManager(ChannelModel channelModel,
|
||||
DecodeConfiguration decodeConfiguration,
|
||||
EventLogConfiguration eventLogConfiguration,
|
||||
RecordConfiguration recordConfiguration,
|
||||
String system,
|
||||
String site,
|
||||
@@ -94,7 +86,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
{
|
||||
mChannelModel = channelModel;
|
||||
mDecodeConfiguration = decodeConfiguration;
|
||||
mEventLogConfiguration = eventLogConfiguration;
|
||||
mRecordConfiguration = recordConfiguration;
|
||||
mSystem = system;
|
||||
mSite = site;
|
||||
@@ -150,8 +141,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
|
||||
channel.setDecodeConfiguration(mDecodeConfiguration);
|
||||
|
||||
channel.setEventLogConfiguration(mEventLogConfiguration);
|
||||
|
||||
channel.setRecordConfiguration(mRecordConfiguration);
|
||||
|
||||
channel.setAliasListName(mAliasListName);
|
||||
@@ -161,7 +150,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
mTrafficChannelPool.add(channel);
|
||||
}
|
||||
|
||||
/* If we have a configured channel, update metadata */
|
||||
/* If we have a configured channel, update metadata */
|
||||
if(channel != null)
|
||||
{
|
||||
channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel));
|
||||
@@ -184,7 +173,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
{
|
||||
CallEvent callEvent = event.getCallEvent();
|
||||
|
||||
/* Check for duplicate events and suppress */
|
||||
/* Check for duplicate events and suppress */
|
||||
synchronized(mTrafficChannelsInUse)
|
||||
{
|
||||
if(mTrafficChannelsInUse.containsKey(callEvent.getChannel()))
|
||||
@@ -196,8 +185,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
|
||||
if(frequency > 0)
|
||||
{
|
||||
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC,
|
||||
frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
|
||||
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency,
|
||||
mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
|
||||
|
||||
if(channel != null)
|
||||
{
|
||||
@@ -213,7 +202,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
else
|
||||
{
|
||||
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
|
||||
callEvent.setCallEventType(CallEventType.CALL_DETECT);
|
||||
|
||||
String details = callEvent.getDetails();
|
||||
|
||||
@@ -230,7 +219,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
else
|
||||
{
|
||||
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
|
||||
callEvent.setCallEventType(CallEventType.CALL_DETECT);
|
||||
|
||||
String details = callEvent.getDetails();
|
||||
|
||||
@@ -247,7 +236,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
else
|
||||
{
|
||||
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
|
||||
callEvent.setCallEventType(CallEventType.CALL_DETECT);
|
||||
|
||||
String details = callEvent.getDetails();
|
||||
|
||||
@@ -337,7 +326,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
public void start()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -348,7 +337,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
{
|
||||
List<String> channels = new ArrayList<>();
|
||||
|
||||
/* Copy the keyset so we don't get concurrent modification of the map */
|
||||
/* Copy the keyset so we don't get concurrent modification of the map */
|
||||
channels.addAll(mTrafficChannelsInUse.keySet());
|
||||
|
||||
for(String channel : channels)
|
||||
@@ -399,4 +388,4 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,18 +31,16 @@ import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigFactory;
|
||||
import io.github.dsheirer.source.config.SourceConfigRecording;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.TunerChannel.Type;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
|
||||
@JacksonXmlRootElement(localName = "channel")
|
||||
public class Channel extends Configuration implements Listener<FrequencyChangeEvent>
|
||||
public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
{
|
||||
// Standard channels are persisted and traffic channels are temporary
|
||||
public enum ChannelType
|
||||
@@ -490,15 +488,14 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
|
||||
{
|
||||
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
|
||||
|
||||
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
|
||||
}
|
||||
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
|
||||
{
|
||||
SourceConfigRecording config =
|
||||
(SourceConfigRecording)mSourceConfiguration;
|
||||
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
|
||||
|
||||
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
|
||||
}
|
||||
}
|
||||
@@ -524,9 +521,9 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
|
||||
* be used to visually show in the spectral display.
|
||||
*/
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent event)
|
||||
public void receive(SourceEvent event)
|
||||
{
|
||||
if(event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
|
||||
if(event.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
|
||||
{
|
||||
mChannelFrequencyCorrection = event.getValue().intValue();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
package io.github.dsheirer.controller.channel;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
|
||||
@@ -35,6 +34,7 @@ import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.record.RecorderManager;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
@@ -54,7 +54,7 @@ public class ChannelProcessingManager implements ChannelEventListener
|
||||
|
||||
private Map<Integer,ProcessingChain> mProcessingChains = new HashMap<>();
|
||||
|
||||
private List<Listener<AudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
|
||||
private List<Listener<ReusableAudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
|
||||
private List<Listener<Message>> mMessageListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
private ChannelModel mChannelModel;
|
||||
@@ -232,7 +232,7 @@ public class ChannelProcessingManager implements ChannelEventListener
|
||||
processingChain = new ProcessingChain(channel.getChannelType());
|
||||
|
||||
/* Register global listeners */
|
||||
for(Listener<AudioPacket> listener : mAudioPacketListeners)
|
||||
for(Listener<ReusableAudioPacket> listener : mAudioPacketListeners)
|
||||
{
|
||||
processingChain.addAudioPacketListener(listener);
|
||||
}
|
||||
@@ -363,11 +363,27 @@ public class ChannelProcessingManager implements ChannelEventListener
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all currently processing channels to prepare for shutdown.
|
||||
*/
|
||||
public void shutdown()
|
||||
{
|
||||
mLog.debug("Stopping Channels ...");
|
||||
for(Channel channel: mChannelModel.getChannels())
|
||||
{
|
||||
if(channel.isProcessing())
|
||||
{
|
||||
mLog.debug("Stopping channel: " + channel.toString());
|
||||
stopProcessing(channel, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message listener that will be added to all channels to receive
|
||||
* any messages.
|
||||
*/
|
||||
public void addAudioPacketListener(Listener<AudioPacket> listener)
|
||||
public void addAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
||||
{
|
||||
mAudioPacketListeners.add(listener);
|
||||
}
|
||||
@@ -375,7 +391,7 @@ public class ChannelProcessingManager implements ChannelEventListener
|
||||
/**
|
||||
* Removes a message listener.
|
||||
*/
|
||||
public void removeAudioPacketListener(Listener<AudioPacket> listener)
|
||||
public void removeAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
||||
{
|
||||
mAudioPacketListeners.remove(listener);
|
||||
}
|
||||
|
||||
@@ -1,77 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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;
|
||||
|
||||
import io.github.dsheirer.bits.IBinarySymbolProcessor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public class NRZDecoder implements Listener<Boolean>
|
||||
public class NRZDecoder implements IBinarySymbolProcessor
|
||||
{
|
||||
public final static boolean MODE_NORMAL = true;
|
||||
public final static boolean MODE_INVERTED = false;
|
||||
|
||||
private Listener<Boolean> mListener;
|
||||
private boolean mMode = MODE_NORMAL;
|
||||
private boolean mPrevious;
|
||||
public final static boolean MODE_NORMAL = true;
|
||||
public final static boolean MODE_INVERTED = false;
|
||||
|
||||
/**
|
||||
* Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
|
||||
*
|
||||
* Use default constructor for NRZ output. Use the alternate constructor
|
||||
* with the MODE_INVERTED argument for NRZ-I output.
|
||||
*
|
||||
* Performs XOR of incoming bit with previous output and sends the result
|
||||
* to the registered listener.
|
||||
*/
|
||||
public NRZDecoder( boolean mode )
|
||||
{
|
||||
mMode = mode;
|
||||
}
|
||||
|
||||
public NRZDecoder()
|
||||
{
|
||||
this( MODE_NORMAL );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
private IBinarySymbolProcessor mListener;
|
||||
private boolean mMode = MODE_NORMAL;
|
||||
private boolean mPrevious;
|
||||
|
||||
@Override
|
||||
public void receive( Boolean bit )
|
||||
/**
|
||||
* Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
|
||||
*
|
||||
* Use default constructor for NRZ output. Use the alternate constructor
|
||||
* with the MODE_INVERTED argument for NRZ-I output.
|
||||
*
|
||||
* Performs XOR of incoming bit with previous output and sends the result
|
||||
* to the registered listener.
|
||||
*/
|
||||
public NRZDecoder(boolean mode)
|
||||
{
|
||||
boolean result = mPrevious ^ bit;
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( mMode ? result : !result );
|
||||
}
|
||||
|
||||
mPrevious = result;
|
||||
mMode = mode;
|
||||
}
|
||||
|
||||
public void setListener( Listener<Boolean> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( Listener<Boolean> listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
public NRZDecoder()
|
||||
{
|
||||
this(MODE_NORMAL);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(boolean symbol)
|
||||
{
|
||||
boolean result = mPrevious ^ symbol;
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(mMode ? result : !result);
|
||||
}
|
||||
|
||||
mPrevious = result;
|
||||
}
|
||||
|
||||
public void setListener(IBinarySymbolProcessor listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener(Listener<Boolean> listener)
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
252
src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java
Normal file
252
src/main/java/io/github/dsheirer/dsp/afsk/AFSK1200Decoder.java
Normal file
@@ -0,0 +1,252 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
import io.github.dsheirer.bits.IBinarySymbolProcessor;
|
||||
import io.github.dsheirer.buffer.FloatAveragingBuffer;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Audio Frequency Shift Keying (AFSK) 1200-baud correlation decoder for decoding 8 kHz FM-demodulated audio samples.
|
||||
*
|
||||
* Note: internally the incoming 8 kHz sample rate is resampled to 7200 Hz to be an integral of the 1200 Hz baud rate
|
||||
* which equates to six samples per symbol. Each symbol is correlated over an eight sample period with correlation
|
||||
* values averaged over a seven correlation value period.
|
||||
*
|
||||
* Provides normal or inverted decoded output.
|
||||
*/
|
||||
public class AFSK1200Decoder implements Listener<ReusableBuffer>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class);
|
||||
|
||||
public enum Output
|
||||
{
|
||||
NORMAL, INVERTED
|
||||
}
|
||||
|
||||
public static final double SAMPLE_RATE = 7200.0d;
|
||||
public static final int SAMPLES_PER_SYMBOL = 6; //SAMPLE_RATE / 1200 baud;
|
||||
|
||||
//Correlation period of 8 samples works well and should align nicely with SIMD intrinsic vector sizes
|
||||
public static final int CORRELATION_PERIOD = SAMPLES_PER_SYMBOL + 2;
|
||||
public static final int AVERAGING_PERIOD = SAMPLES_PER_SYMBOL + 1;
|
||||
public static final double MARK = 1200.0;
|
||||
public static final double SPACE = 1800.0;
|
||||
public static final float TIMING_ERROR_GAIN = 1.0f / 3.0f; //Timing error adjustments over 3 symbol periods
|
||||
|
||||
private Correlator mCorrelatorMark = new Correlator(SAMPLE_RATE, MARK, AVERAGING_PERIOD, CORRELATION_PERIOD);
|
||||
private Correlator mCorrelatorSpace = new Correlator(SAMPLE_RATE, SPACE, AVERAGING_PERIOD, CORRELATION_PERIOD);
|
||||
private float[] mCorrelationValuesMark;
|
||||
private float[] mCorrelationValuesSpace;
|
||||
|
||||
protected boolean mNormalOutput;
|
||||
protected float mSymbolTimingGain = TIMING_ERROR_GAIN;
|
||||
protected AFSKSampleBuffer mSampleBuffer;
|
||||
protected AFSKTimingErrorDetector mTimingErrorDetector = new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL);
|
||||
protected IBinarySymbolProcessor mBinarySymbolProcessor;
|
||||
private boolean mSampleDecision;
|
||||
|
||||
//Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz
|
||||
private RealResampler mResampler = new RealResampler(8000.0, SAMPLE_RATE, 2000, 1);
|
||||
|
||||
/**
|
||||
* Constructs a decoder using the provided arguments.
|
||||
*
|
||||
* @param sampleBuffer for storing incoming samples and calculating symbols
|
||||
* @param detector for timing error alignment
|
||||
* @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa)
|
||||
*/
|
||||
public AFSK1200Decoder(AFSKSampleBuffer sampleBuffer, AFSKTimingErrorDetector detector, Output output)
|
||||
{
|
||||
mTimingErrorDetector = detector;
|
||||
mSampleBuffer = sampleBuffer;
|
||||
mSampleBuffer.setTimingGain(mSymbolTimingGain);
|
||||
mResampler.setListener(new Decoder());
|
||||
mNormalOutput = (output == Output.NORMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a decoder using the provided arguments.
|
||||
*
|
||||
* @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa)
|
||||
*/
|
||||
public AFSK1200Decoder(Output output)
|
||||
{
|
||||
this(new AFSKSampleBuffer(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN),
|
||||
new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL), output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of all references to prepare for garbage collection
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
|
||||
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
|
||||
* adjusted and then each of the symbols are decoded using a simple majority decision.
|
||||
*
|
||||
* @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling.
|
||||
*/
|
||||
@Override
|
||||
public void receive(ReusableBuffer buffer)
|
||||
{
|
||||
mResampler.resample(buffer);
|
||||
}
|
||||
|
||||
protected void dispatch(boolean symbol)
|
||||
{
|
||||
if(mBinarySymbolProcessor != null)
|
||||
{
|
||||
mBinarySymbolProcessor.receive(mNormalOutput ? symbol : !symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive decoded LTR symbols.
|
||||
*
|
||||
* @param binarySymbolProcessor to receive symbols.
|
||||
*/
|
||||
public void setSymbolProcessor(IBinarySymbolProcessor binarySymbolProcessor)
|
||||
{
|
||||
mBinarySymbolProcessor = binarySymbolProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the symbol listener from receiving decoded LTR symbols.
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mBinarySymbolProcessor = null;
|
||||
}
|
||||
|
||||
|
||||
public class Decoder implements Listener<ReusableBuffer>
|
||||
{
|
||||
@Override
|
||||
public void receive(ReusableBuffer buffer)
|
||||
{
|
||||
//Calculate correlation values against each 1200/1800 reference signal
|
||||
mCorrelationValuesMark = mCorrelatorMark.process(buffer);
|
||||
mCorrelationValuesSpace = mCorrelatorSpace.process(buffer);
|
||||
|
||||
buffer.decrementUserCount();
|
||||
|
||||
for(int x = 0; x < mCorrelationValuesMark.length; x++)
|
||||
{
|
||||
//1200 = Mark (1) and 1800 = Space (0)
|
||||
mSampleDecision = mCorrelationValuesMark[x] > mCorrelationValuesSpace[x];
|
||||
mSampleBuffer.receive(mSampleDecision);
|
||||
mTimingErrorDetector.receive(mSampleDecision);
|
||||
|
||||
if(mSampleBuffer.hasSymbol())
|
||||
{
|
||||
dispatch(mSampleBuffer.getSymbol());
|
||||
mSampleBuffer.resetAndAdjust(mTimingErrorDetector.getError());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a correlation value for each FM demodulated sample against samples generated for an established
|
||||
* reference frequency, correlated and averaged over separate periods relative to the samples per symbol period.
|
||||
*
|
||||
* The sample rate of the correlator should be an integral value of the target frequency for optimal performance,
|
||||
* This correlator only works across integral sample periods and does not provide intra-sample interpolation.
|
||||
*
|
||||
* Correlation period (ie number of samples) should be relative to the samples per symbol. You may have to
|
||||
* experiment with various values of (samplesPerSymbol +/- 1, 2 or 3) to obtain the optimal decoder performance.
|
||||
*
|
||||
* Averaging period defines the number of correlation values to average before producing the final correlation
|
||||
* value for each sample.
|
||||
*/
|
||||
public class Correlator
|
||||
{
|
||||
private FloatAveragingBuffer mAveragingBuffer;
|
||||
private float[] mReferenceSamples;
|
||||
private float[] mDemodulatedSamples;
|
||||
private float[] mCorrelationValues;
|
||||
private float mCorrelationAccumulator;
|
||||
|
||||
/**
|
||||
* Constructs a correlator instance,
|
||||
*
|
||||
* @param sampleRate of the incoming sample stream. Note: this should be an integral of the symbol rate.
|
||||
* @param frequency of the mark or space symbol to test for correlation
|
||||
* @param averagingPeriod is the number of correlation values to average each period
|
||||
* @param correlationPeriod is the number of samples to correlate each period
|
||||
*/
|
||||
public Correlator(double sampleRate, double frequency, int averagingPeriod, int correlationPeriod)
|
||||
{
|
||||
mAveragingBuffer = new FloatAveragingBuffer(averagingPeriod);
|
||||
|
||||
IOscillator referenceSignalGenerator = new Oscillator(frequency, sampleRate);
|
||||
mReferenceSamples = referenceSignalGenerator.generateReal(correlationPeriod);
|
||||
|
||||
mDemodulatedSamples = new float[correlationPeriod];
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes each sample in the incoming sample buffer against a generated reference sample set for one symbol
|
||||
* period to derive a correlation value that is in-turn averaged over one symbol period.
|
||||
*
|
||||
* @param reusableBuffer containing FM demodulated samples
|
||||
* @return a reusable array of correlation values for each sample
|
||||
*/
|
||||
public float[] process(ReusableBuffer reusableBuffer)
|
||||
{
|
||||
if(mCorrelationValues == null || mCorrelationValues.length != reusableBuffer.getSampleCount())
|
||||
{
|
||||
mCorrelationValues = new float[reusableBuffer.getSampleCount()];
|
||||
}
|
||||
|
||||
float[] samples = reusableBuffer.getSamples();
|
||||
|
||||
int y;
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
//Note: we're using array copy and dot product structures that the JRE can promote to SIMD intrinsics
|
||||
//when the host processor supports SIMD instructions
|
||||
System.arraycopy(mDemodulatedSamples, 1, mDemodulatedSamples, 0, mDemodulatedSamples.length - 1);
|
||||
mDemodulatedSamples[mDemodulatedSamples.length - 1] = samples[x];
|
||||
|
||||
mCorrelationAccumulator = 0.0f;
|
||||
|
||||
for(y = 0; y < mDemodulatedSamples.length; y++)
|
||||
{
|
||||
mCorrelationAccumulator += mDemodulatedSamples[y] * mReferenceSamples[y];
|
||||
}
|
||||
|
||||
//Add the absolute value of correlation accumulator value to the averaging buffer and store the current
|
||||
//average as the correlation value for this sample. We use absolute value because we don't care if the
|
||||
//signal is out of phase with the reference samples
|
||||
mCorrelationValues[x] = mAveragingBuffer.get(Math.abs(mCorrelationAccumulator));
|
||||
}
|
||||
|
||||
return mCorrelationValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public class AFSK1200DecoderInstrumented extends AFSK1200Decoder
|
||||
{
|
||||
private Broadcaster<Boolean> mSymbolBroadcaster = new Broadcaster<>();
|
||||
|
||||
public AFSK1200DecoderInstrumented(boolean invertOutput)
|
||||
{
|
||||
super(new AFSKSampleBufferInstrumented(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN),
|
||||
new AFSKTimingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL), Output.NORMAL);
|
||||
}
|
||||
|
||||
public AFSKSampleBufferInstrumented getSampleBuffer()
|
||||
{
|
||||
return (AFSKSampleBufferInstrumented)mSampleBuffer;
|
||||
}
|
||||
|
||||
public AFSKTimingErrorDetectorInstrumented getErrorDetector()
|
||||
{
|
||||
return (AFSKTimingErrorDetectorInstrumented)mTimingErrorDetector;
|
||||
}
|
||||
|
||||
protected void dispatch(boolean symbol)
|
||||
{
|
||||
super.dispatch(symbol);
|
||||
mSymbolBroadcaster.broadcast(symbol);
|
||||
}
|
||||
|
||||
public void addListener(Listener<Boolean> listener)
|
||||
{
|
||||
mSymbolBroadcaster.addListener(listener);
|
||||
}
|
||||
}
|
||||
121
src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java
Normal file
121
src/main/java/io/github/dsheirer/dsp/afsk/AFSKSampleBuffer.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AFSKSampleBuffer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AFSKSampleBuffer.class);
|
||||
|
||||
private final static float SYMBOL_SAMPLE_THRESHOLD = 1.0f;
|
||||
protected boolean[] mDelayLine;
|
||||
protected int mDelayLinePointer = 0;
|
||||
|
||||
private int mHalfIntegerSamplesPerSymbol;
|
||||
private int mPositiveSampleDecisionCount;
|
||||
protected int mTwiceSamplesPerSymbol;
|
||||
protected int mSymbolStart;
|
||||
protected int mSymbolEnd;
|
||||
protected int mSamplesPerSymbol;
|
||||
protected float mSymbolSamplingPoint;
|
||||
protected float mSymbolTimingGain;
|
||||
|
||||
/**
|
||||
* Buffer to store complex sample data and produce interpolated samples.
|
||||
* @param samplesPerSymbol
|
||||
*/
|
||||
public AFSKSampleBuffer(int samplesPerSymbol, float symbolTimingGain)
|
||||
{
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mSymbolTimingGain = symbolTimingGain;
|
||||
|
||||
mHalfIntegerSamplesPerSymbol = samplesPerSymbol / 2;
|
||||
mSymbolSamplingPoint = samplesPerSymbol;
|
||||
mTwiceSamplesPerSymbol = 2 * samplesPerSymbol;
|
||||
mDelayLine = new boolean[2 * mTwiceSamplesPerSymbol];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the sample in the sample buffer to support making symbol decisions.
|
||||
*
|
||||
* Note: use the hasSymbol() method to determine if sufficient samples have been collected to make a symbol
|
||||
* determination.
|
||||
*
|
||||
* @param sample to store in the sample buffer
|
||||
*/
|
||||
public void receive(boolean sample)
|
||||
{
|
||||
mSymbolSamplingPoint--;
|
||||
|
||||
mDelayLine[mDelayLinePointer] = sample;
|
||||
mDelayLine[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample;
|
||||
|
||||
//Increment pointer and keep pointer in bounds
|
||||
mDelayLinePointer++;
|
||||
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
|
||||
* symbol.
|
||||
*
|
||||
* @param symbolTimingError from a symbol timing error detector
|
||||
*/
|
||||
public void resetAndAdjust(float symbolTimingError)
|
||||
{
|
||||
mSymbolSamplingPoint += (mSamplesPerSymbol + (symbolTimingError * mSymbolTimingGain));
|
||||
}
|
||||
|
||||
public void setTimingGain(float gain)
|
||||
{
|
||||
mSymbolTimingGain = gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this buffer has accumulated enough samples to represent a full symbol
|
||||
*/
|
||||
public boolean hasSymbol()
|
||||
{
|
||||
return mSymbolSamplingPoint < SYMBOL_SAMPLE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the symbol as a majority decision where the number of positive/true samples are greater than half
|
||||
* of the samples per symbol. Symbol decision is based on the samples collected over the preceeding 1.0 to 2.0
|
||||
* symbol periods.
|
||||
*
|
||||
* @return a true symbol if the majority of the samples were true or positive, otherwise a false symbol.
|
||||
*/
|
||||
public boolean getSymbol()
|
||||
{
|
||||
mPositiveSampleDecisionCount = 0;
|
||||
|
||||
mSymbolStart = mDelayLinePointer;
|
||||
mSymbolEnd = mDelayLinePointer + mSamplesPerSymbol - 1;
|
||||
|
||||
for(int x = mSymbolStart; x <= mSymbolEnd; x++)
|
||||
{
|
||||
if(mDelayLine[x])
|
||||
{
|
||||
mPositiveSampleDecisionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return mPositiveSampleDecisionCount > mHalfIntegerSamplesPerSymbol;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
public class AFSKSampleBufferInstrumented extends AFSKSampleBuffer
|
||||
{
|
||||
public boolean mLastSymbol;
|
||||
|
||||
/**
|
||||
* Buffer to store complex sample data and produce interpolated samples.
|
||||
*
|
||||
* @param samplesPerSymbol
|
||||
* @param symbolTimingGain
|
||||
*/
|
||||
public AFSKSampleBufferInstrumented(int samplesPerSymbol, float symbolTimingGain)
|
||||
{
|
||||
super(samplesPerSymbol, symbolTimingGain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample delay buffer containing boolean (ie positive or negative) samples.
|
||||
*/
|
||||
public boolean[] getDelayLine()
|
||||
{
|
||||
return mDelayLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current delay buffer pointer which points to where the next sample will be stored and/or the oldest sample in
|
||||
* the buffer.
|
||||
*/
|
||||
public int getDelayLinePointer()
|
||||
{
|
||||
return mDelayLinePointer;
|
||||
}
|
||||
|
||||
public int getDelayLineSecondPointer()
|
||||
{
|
||||
return mDelayLinePointer + mTwiceSamplesPerSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current symbol timing gain value
|
||||
* @return
|
||||
*/
|
||||
public float getSymbolTimingGain()
|
||||
{
|
||||
return mSymbolTimingGain;
|
||||
}
|
||||
|
||||
public float getSamplesPerSymbol()
|
||||
{
|
||||
return mSamplesPerSymbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getSymbol()
|
||||
{
|
||||
mLastSymbol = super.getSymbol();
|
||||
return mLastSymbol;
|
||||
}
|
||||
|
||||
public boolean getLastSymbol()
|
||||
{
|
||||
return mLastSymbol;
|
||||
}
|
||||
|
||||
public int getSymbolStart()
|
||||
{
|
||||
return mSymbolStart;
|
||||
}
|
||||
|
||||
public int getSymbolEnd()
|
||||
{
|
||||
return mSymbolEnd;
|
||||
}
|
||||
}
|
||||
122
src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java
Normal file
122
src/main/java/io/github/dsheirer/dsp/afsk/AFSKTest.java
Normal file
@@ -0,0 +1,122 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
import io.github.dsheirer.buffer.FloatAveragingBuffer;
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class AFSKTest
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class);
|
||||
|
||||
private static final DecimalFormat df = new DecimalFormat("00.000");
|
||||
|
||||
public static String format(double value)
|
||||
{
|
||||
if(value > 0)
|
||||
{
|
||||
return " " + df.format(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return df.format(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static float[] generateAFSK1200(boolean[] symbols, int samplesPerSymbol, float gain)
|
||||
{
|
||||
float[] samples = new float[symbols.length * samplesPerSymbol];
|
||||
|
||||
Oscillator oscillator = new Oscillator(1200.0, (double)samplesPerSymbol * 1200);
|
||||
|
||||
for(int x = 0; x < symbols.length; x++)
|
||||
{
|
||||
oscillator.setFrequency(symbols[x] ? 1200.0 : 1800.0);
|
||||
|
||||
for(int y = 0; y < samplesPerSymbol; y++)
|
||||
{
|
||||
Complex sample = new Complex(oscillator.inphase(), oscillator.quadrature());
|
||||
sample.multiply(gain);
|
||||
samples[x * samplesPerSymbol + y] = sample.inphase();
|
||||
oscillator.rotate();
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int sampleSize = 6;
|
||||
|
||||
FloatAveragingBuffer avg1200 = new FloatAveragingBuffer(sampleSize);
|
||||
FloatAveragingBuffer avg1800 = new FloatAveragingBuffer(sampleSize);
|
||||
|
||||
IOscillator oscillator1200 = new Oscillator(1200.0, 7200.0);
|
||||
IOscillator oscillator1800 = new Oscillator(1800.0, 7200.0);
|
||||
oscillator1200.generateReal(1);
|
||||
oscillator1800.generateReal(1);
|
||||
|
||||
float[] reference1200 = oscillator1200.generateReal(sampleSize);
|
||||
float[] reference1800 = oscillator1800.generateReal(sampleSize);
|
||||
|
||||
boolean[] symbols = new boolean[]{true,false,true,false,true,false,true,false,true,true,true,false,true,false,
|
||||
false,false,true,false,false,false,false,true,false,true,true,true,true,false,true,true,false,true,false,
|
||||
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
|
||||
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
|
||||
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
|
||||
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
|
||||
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
|
||||
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
|
||||
true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,
|
||||
false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false};
|
||||
|
||||
float[] samples = generateAFSK1200(symbols, 6, 0.5f);
|
||||
|
||||
mLog.debug("Length:" + samples.length);
|
||||
|
||||
int offset = 0;
|
||||
|
||||
float correlation1200 = 0.0f;
|
||||
float correlation1800 = 0.0f;
|
||||
|
||||
while(offset + sampleSize < samples.length)
|
||||
{
|
||||
correlation1200 = 0.0f;
|
||||
correlation1800 = 0.0f;
|
||||
|
||||
for(int x = 0; x < sampleSize; x++)
|
||||
{
|
||||
correlation1200 += reference1200[x] * samples[x + offset];
|
||||
correlation1800 += reference1800[x] * samples[x + offset];
|
||||
}
|
||||
|
||||
correlation1200 = avg1200.get(Math.abs(correlation1200));
|
||||
correlation1800 = avg1800.get(Math.abs(correlation1800));
|
||||
|
||||
mLog.debug(offset + " 1200:" + format(correlation1200) + " 1800:" + format(correlation1800) +
|
||||
" Decision: " + (correlation1200 > correlation1800 ? "0" : "1"));
|
||||
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
/**
|
||||
* Zero crossing error detector. Provides symbol timing error correction values by detecting zero-crossings within
|
||||
* each symbol period. Ideally there should be only 0 or 1 zero crossings within each symbol period. However, when
|
||||
* there is a slight DC offset in the demodulated samples, there can be up to 2 zero crossings within a symbol
|
||||
* period.
|
||||
*
|
||||
* This detector will only provide error corrections when there are 1 or 2 zero crossings detected. If the number
|
||||
* of zero crossings is greater than 2, then it is assumed that there is no signal present and the returned timing
|
||||
* error value is zero. This detector provides an error correction value relative to (approximately) the middle of
|
||||
* a symbol period and therefore continuously attempts to align zero crossings to the middle of the period.
|
||||
*
|
||||
* Since this detector does NOT use interpolation, the error signal will contain minor levels of symbol timing
|
||||
* jitter. Error signal values are reported as integrals of the sampling period.
|
||||
*/
|
||||
public class AFSKTimingErrorDetector
|
||||
{
|
||||
protected boolean[] mBuffer;
|
||||
protected int mDetectedZeroCrossing;
|
||||
protected float mSymbolMiddle;
|
||||
private int mZeroCrossingCount;
|
||||
|
||||
/**
|
||||
* Constructs the detector for the specified samples per symbol.
|
||||
*
|
||||
* @param samplesPerSymbol
|
||||
*/
|
||||
public AFSKTimingErrorDetector(int samplesPerSymbol)
|
||||
{
|
||||
mBuffer = new boolean[samplesPerSymbol];
|
||||
mSymbolMiddle = (float)(samplesPerSymbol - 1) / 2.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the sample in the buffer. Use the getError() method once enough samples have been processed to determine
|
||||
* the error for a symbol period.
|
||||
*
|
||||
* @param sample
|
||||
*/
|
||||
public void receive(boolean sample)
|
||||
{
|
||||
//Shift all samples left and store new sample at the end
|
||||
System.arraycopy(mBuffer, 1, mBuffer, 0, mBuffer.length - 1);
|
||||
mBuffer[mBuffer.length - 1] = sample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the symbol timing error for the samples processed thus far. This method should be invoked once per
|
||||
* symbol period, triggered via an external samples per symbol tracking process.
|
||||
*
|
||||
* @return symbol timing error calculated as the distance between the approximate symbol center and the next closest
|
||||
* zero crossing index. An error value of zero is returned if the number of zero crossings is less than one or
|
||||
* greater than two.
|
||||
*/
|
||||
public int getError()
|
||||
{
|
||||
mZeroCrossingCount = 0;
|
||||
|
||||
for(int x = 0; x < mBuffer.length - 1; x++)
|
||||
{
|
||||
if(mBuffer[x] ^ mBuffer[x + 1])
|
||||
{
|
||||
mDetectedZeroCrossing = x + 1;
|
||||
mZeroCrossingCount++;
|
||||
|
||||
if(mZeroCrossingCount > 1)
|
||||
{
|
||||
return 0; //Too many crossings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mZeroCrossingCount == 1)
|
||||
{
|
||||
if(mDetectedZeroCrossing > mSymbolMiddle)
|
||||
{
|
||||
return -(mBuffer.length - mDetectedZeroCrossing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return mDetectedZeroCrossing + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.afsk;
|
||||
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
public class AFSKTimingErrorDetectorInstrumented extends AFSKTimingErrorDetector
|
||||
{
|
||||
public SimpleIntegerProperty timingError = new SimpleIntegerProperty();
|
||||
|
||||
/**
|
||||
* Constructs the detector for the specified samples per symbol.
|
||||
*
|
||||
* @param samplesPerSymbol
|
||||
*/
|
||||
public AFSKTimingErrorDetectorInstrumented(int samplesPerSymbol)
|
||||
{
|
||||
super(samplesPerSymbol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getError()
|
||||
{
|
||||
int error = super.getError();
|
||||
|
||||
timingError.setValue(error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public int getDetectedZeroCrossing()
|
||||
{
|
||||
return mDetectedZeroCrossing;
|
||||
}
|
||||
|
||||
public boolean[] getBuffer()
|
||||
{
|
||||
return mBuffer;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.am;
|
||||
|
||||
public abstract class AMDemodulator
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
|
||||
/**
|
||||
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
|
||||
*/
|
||||
public class AMDemodulator
|
||||
{
|
||||
private float mGain;
|
||||
|
||||
public AMDemodulator( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public float demodulate( float inphase, float quadrature )
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("AMDemodulator");
|
||||
private int mOutputBufferPointer;
|
||||
private float mGain;
|
||||
|
||||
/**
|
||||
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
|
||||
*
|
||||
* @param gain to apply to demodulated output samples.
|
||||
*/
|
||||
public AMDemodulator(float gain)
|
||||
{
|
||||
return (float)Math.sqrt( ( inphase * inphase ) +
|
||||
( quadrature * quadrature ) ) * mGain;
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the gain to the specified level.
|
||||
*/
|
||||
public void setGain(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demodulates the comples I/Q sample and returns the demodulated output sample.
|
||||
*
|
||||
* @param inphase sample
|
||||
* @param quadrature sample
|
||||
* @return AM demodulated sample
|
||||
*/
|
||||
public float demodulate(float inphase, float quadrature)
|
||||
{
|
||||
return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Demodulates the reusable comples sample buffer and returns a demodulated audio buffer.
|
||||
* @param complexBuffer to demodulate
|
||||
* @return demodulated audio buffer.
|
||||
*/
|
||||
public ReusableBuffer demodulate(ReusableComplexBuffer complexBuffer)
|
||||
{
|
||||
ReusableBuffer reusableBuffer = mReusableBufferQueue.getBuffer(complexBuffer.getSampleCount());
|
||||
float[] input = complexBuffer.getSamples();
|
||||
float[] output = reusableBuffer.getSamples();
|
||||
mOutputBufferPointer = 0;
|
||||
|
||||
for(int x= 0; x < complexBuffer.getSamples().length; x += 2)
|
||||
{
|
||||
output[mOutputBufferPointer++] = demodulate(input[x], input[x + 1]);
|
||||
}
|
||||
|
||||
complexBuffer.decrementUserCount();
|
||||
|
||||
return reusableBuffer;
|
||||
}
|
||||
|
||||
public void setGain( float gain )
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* 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/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.am;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class AMDemodulator_CB extends AMDemodulator implements Listener<ComplexBuffer>
|
||||
{
|
||||
private Listener<RealBuffer> mListener;
|
||||
|
||||
public AMDemodulator_CB( float gain )
|
||||
{
|
||||
super( gain );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
int half = samples.length / 2;
|
||||
|
||||
float[] demodulated = new float[ half ];
|
||||
|
||||
for( int x = 0; x < half; x++ )
|
||||
{
|
||||
int index = x * 2;
|
||||
|
||||
demodulated[ x ] = demodulate( samples[ index ], samples[ index + 1 ] );
|
||||
}
|
||||
|
||||
mListener.receive( new RealBuffer( demodulated ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.filter;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
@Deprecated
|
||||
public abstract class ComplexFilter implements Listener<Complex>
|
||||
{
|
||||
private Listener<Complex> mListener;
|
||||
|
||||
public void setListener( Listener<Complex> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public Listener<Complex> getListener()
|
||||
{
|
||||
return mListener;
|
||||
}
|
||||
|
||||
public boolean hasListener()
|
||||
{
|
||||
return mListener != null;
|
||||
}
|
||||
|
||||
protected void send( Complex sample )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( sample );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -34,10 +35,14 @@ import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Utility methods for designing various types of filters.
|
||||
*/
|
||||
public class FilterFactory
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger(FilterFactory.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class);
|
||||
private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0)
|
||||
private static final double MARGIN_OF_ERROR = 0.0003;
|
||||
|
||||
/**
|
||||
* Generates coefficients for a unity-gain, windowed low-pass filter
|
||||
@@ -48,43 +53,49 @@ public class FilterFactory
|
||||
* @param window - to apply against the coefficients
|
||||
* @return
|
||||
*/
|
||||
public static float[] getSinc(int sampleRate,
|
||||
long frequency,
|
||||
int length,
|
||||
Window.WindowType window)
|
||||
public static float[] getSinc(double sampleRate, long frequency, int length, Window.WindowType window)
|
||||
{
|
||||
//Ensure we have an odd length
|
||||
assert (length % 2 == 0);
|
||||
int evenLength = length % 2 == 0 ? length : length + 1;
|
||||
|
||||
//Get unity response array (one element longer to align with IDFT size)
|
||||
float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, length + 1);
|
||||
float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, evenLength);
|
||||
|
||||
//Apply Inverse DFT against frequency response unity values, leaving the
|
||||
//IDFT bin results in the frequency response array
|
||||
FloatFFT_1D idft = new FloatFFT_1D(length + 1);
|
||||
idft.realInverseFull(frequencyResponse, false);
|
||||
FloatFFT_1D idft = new FloatFFT_1D(evenLength);
|
||||
idft.realInverseFull(frequencyResponse, true);
|
||||
|
||||
//Transfer the IDFT results to the return array
|
||||
float[] coefficients = new float[length];
|
||||
int middleCoefficient = (int) (length / 2);
|
||||
int middleCoefficient = (int)(length / 2);
|
||||
|
||||
//Bin 0 of the idft is our center coefficient
|
||||
coefficients[middleCoefficient] = frequencyResponse[0];
|
||||
|
||||
//The remaining idft bins from 1 to (middle - 1) are the mirror image
|
||||
//coefficients
|
||||
for(int x = 1; x < middleCoefficient; x++)
|
||||
if(length % 2 == 0) //Even length
|
||||
{
|
||||
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
|
||||
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
|
||||
//The remaining idft bins from 1 to (middle - 1) are the mirror image
|
||||
//coefficients
|
||||
for(int x = 0; x < middleCoefficient; x++)
|
||||
{
|
||||
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
|
||||
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Bin 0 of the idft is our center coefficient
|
||||
coefficients[middleCoefficient] = frequencyResponse[0];
|
||||
|
||||
//The remaining idft bins from 1 to (middle - 1) are the mirror image
|
||||
//coefficients
|
||||
for(int x = 1; x < middleCoefficient; x++)
|
||||
{
|
||||
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
|
||||
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
|
||||
}
|
||||
}
|
||||
|
||||
//Apply the window against the coefficients
|
||||
coefficients = Window.apply(window, coefficients);
|
||||
|
||||
//Normalize to unity (1) gain
|
||||
coefficients = normalize(coefficients);
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
@@ -95,7 +106,7 @@ public class FilterFactory
|
||||
* @param coefficients
|
||||
* @return
|
||||
*/
|
||||
private static float[] normalize(float[] coefficients)
|
||||
public static float[] normalize(float[] coefficients)
|
||||
{
|
||||
float accumulator = 0;
|
||||
|
||||
@@ -112,6 +123,32 @@ public class FilterFactory
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all filter coefficients to achieve an overall objective gain, by ensuring
|
||||
* that the sum of the absolute value of all coefficients adds up to the objective.
|
||||
*
|
||||
* @param coefficients
|
||||
* @return
|
||||
*/
|
||||
public static float[] normalize(float[] coefficients, float objective)
|
||||
{
|
||||
float accumulator = 0;
|
||||
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
accumulator += Math.abs(coefficients[x]);
|
||||
}
|
||||
|
||||
accumulator /= objective;
|
||||
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
coefficients[x] = coefficients[x] / accumulator;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an array of unity (1) response values representing the
|
||||
* desired (pre-windowing) frequency response, used by the sync function
|
||||
@@ -128,17 +165,15 @@ public class FilterFactory
|
||||
* @param length
|
||||
* @return
|
||||
*/
|
||||
public static float[] getUnityResponseArray(int sampleRate,
|
||||
long frequency,
|
||||
int length)
|
||||
public static float[] getUnityResponseArray(double sampleRate, long frequency, int length)
|
||||
{
|
||||
float[] unityArray = new float[length * 2];
|
||||
|
||||
int binCount = (int) ((Math.round(
|
||||
(float) frequency / (float) sampleRate * (float) length)));
|
||||
|
||||
if(length % 2 == 0) //even length
|
||||
{
|
||||
int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length)));
|
||||
|
||||
for(int x = 0; x < binCount; x++)
|
||||
{
|
||||
unityArray[x] = 1.0f;
|
||||
@@ -147,9 +182,11 @@ public class FilterFactory
|
||||
}
|
||||
else //odd length
|
||||
{
|
||||
int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length + 0.5)));
|
||||
|
||||
unityArray[0] = 1.0f;
|
||||
|
||||
for(int x = 1; x <= binCount; x++)
|
||||
for(int x = 1; x < binCount; x++)
|
||||
{
|
||||
unityArray[x] = 1.0f;
|
||||
unityArray[length - x] = 1.0f;
|
||||
@@ -159,25 +196,6 @@ public class FilterFactory
|
||||
return unityArray;
|
||||
}
|
||||
|
||||
public static float[] getSine(float sampleRate, float frequency, int length)
|
||||
{
|
||||
float[] retVal = new float[length];
|
||||
|
||||
float radianFrequency = (float) (2.0d * Math.PI * (frequency / sampleRate));
|
||||
|
||||
int middle = (int) (length / 2);
|
||||
|
||||
for(int x = 0; x < middle; x++)
|
||||
{
|
||||
float val = (float) Math.sin(radianFrequency * x);
|
||||
|
||||
retVal[middle + x] = val;
|
||||
retVal[middle - x] = -val;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a repeating sequence of 1, -1 to the coefficients to invert
|
||||
* the frequency response of the filter. Used in converting a low-pass
|
||||
@@ -211,25 +229,9 @@ public class FilterFactory
|
||||
* @param windowType - window to apply against the generated coefficients
|
||||
* @return
|
||||
*/
|
||||
public static float[] getLowPass(int sampleRate,
|
||||
long cutoff,
|
||||
int filterLength,
|
||||
Window.WindowType windowType)
|
||||
public static float[] getLowPass(double sampleRate, long cutoff, int filterLength, Window.WindowType windowType)
|
||||
{
|
||||
if(filterLength % 2 == 0) //even length
|
||||
{
|
||||
float[] values = getSinc(sampleRate, cutoff, filterLength + 2, windowType);
|
||||
|
||||
//throw away the 0 index and the last index
|
||||
return Arrays.copyOfRange(values, 1, values.length - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
float[] values = getSinc(sampleRate, cutoff, filterLength + 1, windowType);
|
||||
|
||||
//throw away the 0 index
|
||||
return Arrays.copyOfRange(values, 1, values.length);
|
||||
}
|
||||
return getSinc(sampleRate, cutoff, filterLength, windowType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,26 +248,17 @@ public class FilterFactory
|
||||
* - passFrequency < stopFrequency
|
||||
* - stopFrequency <= sampleRate/2
|
||||
*/
|
||||
public static float[] getLowPass(int sampleRate,
|
||||
int passFrequency,
|
||||
int stopFrequency,
|
||||
int attenuation,
|
||||
Window.WindowType windowType,
|
||||
boolean forceOddLength)
|
||||
public static float[] getLowPass(double sampleRate, int passFrequency, int stopFrequency, int attenuation,
|
||||
Window.WindowType windowType, boolean forceOddLength)
|
||||
{
|
||||
if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2))
|
||||
{
|
||||
throw new IllegalArgumentException("FilterFactory - low pass "
|
||||
+ "filter pass frequency [" + passFrequency + "] must be "
|
||||
+ "less than the stop frequency [" + stopFrequency + "] "
|
||||
+ "and must be less than half [" + (int) (sampleRate / 2) +
|
||||
"] of the sample rate [" + sampleRate + "]");
|
||||
throw new IllegalArgumentException("FilterFactory - low pass filter pass frequency [" + passFrequency +
|
||||
"] must be less than the stop frequency [" + stopFrequency + "] and must be less than half [" +
|
||||
(int)(sampleRate / 2) + "] of the sample rate [" + sampleRate + "]");
|
||||
}
|
||||
|
||||
int tapCount = getTapCount(sampleRate,
|
||||
passFrequency,
|
||||
stopFrequency,
|
||||
attenuation);
|
||||
int tapCount = getTapCount(sampleRate, passFrequency, stopFrequency, attenuation);
|
||||
|
||||
if(forceOddLength)
|
||||
{
|
||||
@@ -289,34 +282,21 @@ public class FilterFactory
|
||||
* @param windowType - window to apply against the generated coefficients
|
||||
* @return
|
||||
*/
|
||||
public static float[] getHighPass(int sampleRate,
|
||||
long cutoff,
|
||||
int filterLength,
|
||||
Window.WindowType windowType)
|
||||
public static float[] getHighPass(int sampleRate, long cutoff, int filterLength, Window.WindowType windowType)
|
||||
{
|
||||
//Convert the high frequency cutoff to its low frequency cutoff
|
||||
//equivalent, so that when we generate the low pass filter, prior to
|
||||
//inversion, its at the correct frequency
|
||||
long convertedCutoff = sampleRate / 2 - cutoff;
|
||||
|
||||
return invert(getSinc(sampleRate,
|
||||
convertedCutoff,
|
||||
filterLength,
|
||||
windowType));
|
||||
return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType));
|
||||
}
|
||||
|
||||
public static float[] getHighPass(int sampleRate,
|
||||
long stopFrequency,
|
||||
long passFrequency,
|
||||
int attenuation,
|
||||
Window.WindowType windowType,
|
||||
boolean forceOddLength)
|
||||
public static float[] getHighPass(int sampleRate, long stopFrequency, long passFrequency, int attenuation,
|
||||
Window.WindowType windowType, boolean forceOddLength)
|
||||
{
|
||||
/* reverse the stop and pass frequency to get the low pass variant */
|
||||
int tapCount = getTapCount(sampleRate,
|
||||
stopFrequency,
|
||||
passFrequency,
|
||||
attenuation);
|
||||
int tapCount = getTapCount(sampleRate, stopFrequency, passFrequency, attenuation);
|
||||
|
||||
if(forceOddLength)
|
||||
{
|
||||
@@ -360,11 +340,10 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the number of fir filter taps required to produce the
|
||||
* specified frequency response with passband ripple near .1dB.
|
||||
* Determines the number of fir filter taps required to produce the specified frequency response with passband
|
||||
* ripple near 0.1dB.
|
||||
*
|
||||
* Implements the algorithm from Understanding Digital Signal Processing, 3e
|
||||
* , Lyons, section 5.10.5.
|
||||
* Implements the algorithm from Understanding Digital Signal Processing, 3e, Lyons, section 5.10.5.
|
||||
*
|
||||
* @param sampleRate in hertz
|
||||
* @param pass pass frequency in hertz
|
||||
@@ -372,21 +351,17 @@ public class FilterFactory
|
||||
* @param attenuation in dB
|
||||
* @return
|
||||
*/
|
||||
public static int getTapCount(int sampleRate,
|
||||
long pass,
|
||||
long stop,
|
||||
int attenuation)
|
||||
public static int getTapCount(double sampleRate, long pass, long stop, int attenuation)
|
||||
{
|
||||
double frequency = ((double) stop - (double) pass) / (double) sampleRate;
|
||||
double frequency = ((double)stop - (double)pass) / sampleRate;
|
||||
|
||||
return (int) (Math.round((double) attenuation / (22.0d * frequency)));
|
||||
return (int)(Math.round((double)attenuation / (22.0d * frequency)));
|
||||
}
|
||||
|
||||
public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate,
|
||||
int decimatedRate, int order, int passFrequency, int attenuation,
|
||||
Window.WindowType windowType)
|
||||
public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate, int decimatedRate, int order,
|
||||
int passFrequency, int attenuation, Window.WindowType windowType)
|
||||
{
|
||||
int decimationRate = (int) (sampleRate / decimatedRate);
|
||||
int decimationRate = (int)(sampleRate / decimatedRate);
|
||||
|
||||
return new ComplexPrimeCICDecimate(decimationRate, order,
|
||||
passFrequency, attenuation, windowType);
|
||||
@@ -401,10 +376,7 @@ public class FilterFactory
|
||||
* polyphase filter decimation chain
|
||||
* @throws - AssertionException if sample rate is not a multiple of 48 kHz
|
||||
*/
|
||||
public static int[] getPolyphaseDecimationRates(int sampleRate,
|
||||
int decimatedRate,
|
||||
long passFrequency,
|
||||
long stopFrequency)
|
||||
public static int[] getPolyphaseDecimationRates(int sampleRate, int decimatedRate, long passFrequency, long stopFrequency)
|
||||
{
|
||||
int[] rates;
|
||||
|
||||
@@ -414,7 +386,7 @@ public class FilterFactory
|
||||
+ "integer multiple of sample rate");
|
||||
}
|
||||
|
||||
int decimation = (int) (sampleRate / decimatedRate);
|
||||
int decimation = (int)(sampleRate / decimatedRate);
|
||||
|
||||
//Decimation rates below 20 will use single stage polyphase filter
|
||||
if(decimation < 20)
|
||||
@@ -432,11 +404,6 @@ public class FilterFactory
|
||||
|
||||
int stage1 = findClosest(optimalStage1, factors);
|
||||
|
||||
// mLog.info( "Decimation rate [" + decimation +
|
||||
// "] stage1 optimal [" + optimalStage1 +
|
||||
// "] stage1 actual [" + stage1 +
|
||||
// "]");
|
||||
|
||||
if(stage1 == decimation || stage1 == 1)
|
||||
{
|
||||
rates = new int[1];
|
||||
@@ -447,7 +414,7 @@ public class FilterFactory
|
||||
{
|
||||
rates = new int[2];
|
||||
|
||||
int stage2 = (int) (decimation / stage1);
|
||||
int stage2 = (int)(decimation / stage1);
|
||||
|
||||
if(stage1 > stage2)
|
||||
{
|
||||
@@ -465,8 +432,7 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the factor that is closest to the desired factor, from an ordered
|
||||
* list of factors.
|
||||
* Finds the factor that is closest to the desired factor, from an ordered list of factors.
|
||||
*/
|
||||
private static int findClosest(int desiredFactor, Set<Integer> factors)
|
||||
{
|
||||
@@ -488,9 +454,8 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the factors that make up an integer. Uses a brute force
|
||||
* method to iterate all integers from 1 to value, determining which factors
|
||||
* are evenly divisible into the value.
|
||||
* Determines the factors that make up an integer. Uses a brute force method to iterate all integers from 1 to
|
||||
* value, determining which factors are evenly divisible into the value.
|
||||
*
|
||||
* @param value - integer decimation value
|
||||
* @return - ordered set of factors for value
|
||||
@@ -498,11 +463,11 @@ public class FilterFactory
|
||||
private static Set<Integer> getFactors(int value)
|
||||
{
|
||||
Set<Integer> factors = new TreeSet<Integer>();
|
||||
|
||||
/* Brute force */
|
||||
|
||||
/* Brute force */
|
||||
for(int x = 1; x <= value; x++)
|
||||
{
|
||||
int remainder = (int) (value / x);
|
||||
int remainder = (int)(value / x);
|
||||
|
||||
if(remainder * x == value)
|
||||
{
|
||||
@@ -514,14 +479,10 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the optimal decimation rate for stage 1 of a two-stage
|
||||
* poly-phase decimation filter chain, to produce a final sample rate of
|
||||
* 48 kHz using a pass bandwidth of 25 kHz.
|
||||
* Determines the optimal decimation rate for stage 1 of a two-stage poly-phase decimation filter chain, to produce
|
||||
* a final sample rate of 48 kHz using a pass bandwidth of 25 kHz. Use for total decimation rates of 20 or higher.
|
||||
*
|
||||
* Use for total decimation rates of 20 or higher.
|
||||
*
|
||||
* Implements the algorithm described in Lyons, Understanding Digital Signal
|
||||
* Processing, 3e, section 10.2.1, page 511.
|
||||
* Implements the algorithm described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511.
|
||||
*
|
||||
* @param sampleRate
|
||||
* @param decimation
|
||||
@@ -529,42 +490,29 @@ public class FilterFactory
|
||||
* @return optimum integer decimation rate for the first stage decimation
|
||||
* filter
|
||||
*/
|
||||
public static int getOptimalStageOneRate(int sampleRate,
|
||||
int decimation,
|
||||
long passFrequency,
|
||||
long stopFrequency)
|
||||
public static int getOptimalStageOneRate(int sampleRate, int decimation, long passFrequency, long stopFrequency)
|
||||
{
|
||||
double ratio = getBandwidthRatio(passFrequency, stopFrequency);
|
||||
|
||||
double numerator = 1.0d - (
|
||||
Math.sqrt((double) decimation * ratio / (2.0d - ratio)));
|
||||
double numerator = 1.0d - (Math.sqrt((double)decimation * ratio / (2.0d - ratio)));
|
||||
|
||||
double denominator = 2.0d - (ratio * (decimation + 1.0d));
|
||||
|
||||
int retVal = (int) (2.0d * decimation * (numerator / denominator));
|
||||
|
||||
// mLog.info( "Optimal Stage 1 Decimation - rate [" + sampleRate +
|
||||
// "] pass [" + passFrequency +
|
||||
// "] bw ratio [" + ratio +
|
||||
// "] optimal [" + retVal +
|
||||
// "]" );
|
||||
int retVal = (int)(2.0d * decimation * (numerator / denominator));
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the F ratio as described in Lyons, Understanding Digital
|
||||
* Signal Processing, 3e, section 10.2.1, page 511
|
||||
*
|
||||
* Used in conjunction with the optimal stage one decimation rate method
|
||||
* above.
|
||||
* Determines the F ratio as described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511
|
||||
* Used in conjunction with the optimal stage one decimation rate method above.
|
||||
*/
|
||||
private static double getBandwidthRatio(long passFrequency, long stopFrequency)
|
||||
{
|
||||
assert (passFrequency < stopFrequency);
|
||||
|
||||
return ((double) (stopFrequency - passFrequency) /
|
||||
(double) stopFrequency);
|
||||
return ((double)(stopFrequency - passFrequency) /
|
||||
(double)stopFrequency);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -575,15 +523,12 @@ public class FilterFactory
|
||||
* @param outputSampleRate
|
||||
* @return
|
||||
*/
|
||||
public static float[] getCICCleanupFilter(int outputSampleRate,
|
||||
int passFrequency,
|
||||
int attenuation,
|
||||
Window.WindowType window)
|
||||
public static float[] getCICCleanupFilter(int outputSampleRate, int passFrequency, int attenuation, Window.WindowType window)
|
||||
{
|
||||
int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500,
|
||||
attenuation);
|
||||
|
||||
/* Make tap count odd */
|
||||
|
||||
/* Make tap count odd */
|
||||
if(taps % 2 == 0)
|
||||
{
|
||||
taps++;
|
||||
@@ -599,7 +544,7 @@ public class FilterFactory
|
||||
|
||||
//Transfer the IDFT results to the odd length return array
|
||||
float[] coefficients = new float[taps];
|
||||
int middleCoefficient = (int) (taps / 2);
|
||||
int middleCoefficient = (int)(taps / 2);
|
||||
|
||||
//Bin 0 of the idft is our center coefficient
|
||||
coefficients[middleCoefficient] = frequencyResponse[0];
|
||||
@@ -620,26 +565,22 @@ public class FilterFactory
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
public static float[] getCICResponseArray(int sampleRate,
|
||||
int frequency,
|
||||
int length)
|
||||
public static float[] getCICResponseArray(int sampleRate, int frequency, int length)
|
||||
{
|
||||
float[] cicArray = new float[length * 2];
|
||||
|
||||
int binCount = (int) ((Math.round(
|
||||
(double) frequency / (double) sampleRate * 2.0d * (double) length)));
|
||||
int binCount = (int)((Math.round(
|
||||
(double)frequency / (double)sampleRate * 2.0d * (double)length)));
|
||||
|
||||
cicArray[0] = 1.0f;
|
||||
|
||||
float unityResponse = (float) (Math.sin(1.0d / (double) length) /
|
||||
(1.0d / (double) length));
|
||||
float unityResponse = (float)(Math.sin(1.0d / (double)length) /
|
||||
(1.0d / (double)length));
|
||||
|
||||
for(int x = 1; x <= binCount; x++)
|
||||
{
|
||||
/* Calculate unity response plus amplification for droop */
|
||||
float compensated = 1.0f + (unityResponse -
|
||||
(float) (Math.sin((double) x / (double) length) /
|
||||
((double) x / (double) length)));
|
||||
/* Calculate unity response plus amplification for droop */
|
||||
float compensated = 1.0f + (unityResponse - (float)(Math.sin((double)x / (double)length) / ((double)x / (double)length)));
|
||||
|
||||
cicArray[x] = compensated;
|
||||
cicArray[length - x] = compensated;
|
||||
@@ -649,28 +590,29 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates root raised cosine filter coefficients with a tap count equal
|
||||
* to the symbols x samplesPerSymbol + 1.
|
||||
* Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1.
|
||||
*
|
||||
* Symbol count should be an even integer.
|
||||
*
|
||||
* Ported to java from gnuradio/filter/firdes.cc
|
||||
*
|
||||
* For 40db attenuation, calculate the number of symbols based on the
|
||||
* following formula:
|
||||
*
|
||||
* For 40db attenuation, calculate the number of symbols based on the following formula:
|
||||
* Symbols = -44 * alpha + 33
|
||||
*
|
||||
* Polyphase Channelizer notes:
|
||||
* -Set samples Per Symbol at 2 (or more) * number of channels
|
||||
* -Set symbolCount to sufficient size to produce required attenuation
|
||||
* -Set symbol rate in hertz
|
||||
* -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0
|
||||
* -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha
|
||||
*
|
||||
* @param samplesPerSymbol - number of samples per symbol
|
||||
* @param symbols - number of symbols - must be even
|
||||
* @param alpha - roll-off factor
|
||||
* @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size
|
||||
* @param alpha - roll-off factor.
|
||||
* @return - filter coefficients
|
||||
*/
|
||||
public static float[] getRootRaisedCosine(float samplesPerSymbol,
|
||||
int symbols,
|
||||
float alpha)
|
||||
public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha)
|
||||
{
|
||||
int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
|
||||
int taps = samplesPerSymbol * symbolCount;
|
||||
|
||||
float scale = 0;
|
||||
|
||||
@@ -678,10 +620,10 @@ public class FilterFactory
|
||||
|
||||
for(int x = 0; x < taps; x++)
|
||||
{
|
||||
float index = (float) x - ((float) taps / 2.0f);
|
||||
float index = (float)x - ((float)taps / 2.0f);
|
||||
|
||||
float x1 = (float) Math.PI * index / samplesPerSymbol;
|
||||
float x2 = 4.0f * alpha * index / samplesPerSymbol;
|
||||
float x1 = (float)Math.PI * index / (float)samplesPerSymbol;
|
||||
float x2 = 4.0f * alpha * index / (float)samplesPerSymbol;
|
||||
float x3 = x2 * x2 - 1.0f;
|
||||
|
||||
float numerator, denominator;
|
||||
@@ -690,17 +632,15 @@ public class FilterFactory
|
||||
{
|
||||
if(x != taps / 2)
|
||||
{
|
||||
numerator = (float) Math.cos((1.0 + alpha) * x1) +
|
||||
(float) Math.sin((1.0f - alpha) * x1) /
|
||||
(4.0f * alpha * index / samplesPerSymbol);
|
||||
numerator = (float)Math.cos((1.0 + alpha) * x1) +
|
||||
(float)Math.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol);
|
||||
}
|
||||
else
|
||||
{
|
||||
numerator = (float) Math.cos((1.0f + alpha) * x1) +
|
||||
(1.0f - alpha) * (float) Math.PI / (4.0f * alpha);
|
||||
numerator = (float)Math.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)Math.PI / (4.0f * alpha);
|
||||
}
|
||||
|
||||
denominator = x3 * (float) Math.PI;
|
||||
denominator = x3 * (float)Math.PI;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -713,14 +653,11 @@ public class FilterFactory
|
||||
x3 = (1.0f - alpha) * x1;
|
||||
x2 = (1.0f + alpha) * x1;
|
||||
|
||||
numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI -
|
||||
Math.cos(x3) * ((1.0 - alpha) * Math.PI *
|
||||
(double) samplesPerSymbol) / (4.0 * alpha * index) +
|
||||
Math.sin(x3) * (double) samplesPerSymbol *
|
||||
(double) samplesPerSymbol / (4.0 * alpha * index * index));
|
||||
numerator = (float)(Math.sin(x2) * (1.0f + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI *
|
||||
(double)samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double)samplesPerSymbol *
|
||||
(double)samplesPerSymbol / (4.0 * alpha * index * index));
|
||||
|
||||
denominator = (float) (-32.0 * Math.PI * alpha * alpha * index /
|
||||
(double) samplesPerSymbol);
|
||||
denominator = (float)(-32.0 * Math.PI * alpha * alpha * index / (double)samplesPerSymbol);
|
||||
}
|
||||
|
||||
coefficients[x] = 4.0f * alpha * numerator / denominator;
|
||||
@@ -740,10 +677,10 @@ public class FilterFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a filter from the filter specification
|
||||
* Creates a filter from the filter specification using the remez exchange design algorithm
|
||||
*
|
||||
* @param specification
|
||||
* @return
|
||||
* @return filter coefficients
|
||||
* @throws FilterDesignException if the filter cannot be designed
|
||||
*/
|
||||
public static float[] getTaps(FIRFilterSpecification specification) throws FilterDesignException
|
||||
@@ -757,4 +694,346 @@ public class FilterFactory
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the filter to determine the magnitude response (dB) at the specified frequency.
|
||||
*
|
||||
* @param filter to evaluate
|
||||
* @param frequency to calculate (0 <> 0.5)
|
||||
* @return magnitude frequency response in decibels
|
||||
*/
|
||||
public static double evaluate(float[] filter, double frequency)
|
||||
{
|
||||
double real = 0.0;
|
||||
double imag = 0.0;
|
||||
|
||||
for(int x = 0; x < filter.length; x++)
|
||||
{
|
||||
real += filter[x] * Math.cos(Math.PI * frequency * (double)x);
|
||||
imag += filter[x] * Math.sin(Math.PI * frequency * (double)x);
|
||||
}
|
||||
|
||||
return decibel(real, imag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the decibel magnitude of the real and imaginary complex sample
|
||||
*
|
||||
* @param real (x-axis) value
|
||||
* @param imag (y-axis) value
|
||||
* @return magnitude in decibels
|
||||
*/
|
||||
public static double decibel(double real, double imag)
|
||||
{
|
||||
return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0)));
|
||||
}
|
||||
|
||||
|
||||
public static float[] getRemezChannelizer(int channelBandwidth, int channels, int tapsPerChannel, double alpha,
|
||||
double passRipple, double stopRipple) throws FilterDesignException
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.channelizerBuilder()
|
||||
.sampleRate(channels * channelBandwidth * 4)
|
||||
.channels(channels)
|
||||
.channelBandwidth(channelBandwidth)
|
||||
.tapsPerChannel(tapsPerChannel)
|
||||
.alpha(alpha)
|
||||
.passRipple(passRipple)
|
||||
.stopRipple(stopRipple)
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification);
|
||||
|
||||
float[] taps = designer.getImpulseResponse();
|
||||
|
||||
double bandEdgeFrequency = (double)channelBandwidth / (double)(channels * channelBandwidth * 2);
|
||||
|
||||
double response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI)), 0.0);
|
||||
mLog.debug("Frequency Response at 1.0: " + response);
|
||||
response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI * 2.0)), 0.0);
|
||||
mLog.debug("Frequency Response at 2.0: " + response);
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase M2 synthesizer sync filter. Designed for multiple M2 oversampled channel inputs and
|
||||
* an M*channel count output.
|
||||
*
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @return filter
|
||||
* @throws FilterDesignException
|
||||
*/
|
||||
public static float[] getSincM2Synthesizer(double channelBandwidth, int channels, int tapsPerChannel)
|
||||
throws FilterDesignException
|
||||
{
|
||||
int filterLength = (channels * tapsPerChannel) - 1;
|
||||
|
||||
//Design the prototype synthesizer with 105% of the channel bandwidth produced by the channelizer.
|
||||
float[] taps = FilterFactory.getKaiserSinc(filterLength, channelBandwidth * 1.05, 80.0);
|
||||
|
||||
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
|
||||
float[] extendedTaps = new float[taps.length + 1];
|
||||
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
|
||||
|
||||
//Adjust the overall gain of the filter to an objective of 1.0 divided by the number of channels
|
||||
return normalize(extendedTaps, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a windowed-sync (Nyquist) M/2 prototype FIR filter for use with a Polyphase Channelizer/Synthesizer.
|
||||
* The filter is designed for x2 oversampling of each channel and the filter cutoff frequency is incrementally
|
||||
* adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent
|
||||
* channels.
|
||||
*
|
||||
* The odd length sync filter that meets the design objectives will be increased in length by pre-padding a zero
|
||||
* coefficient to produce a filter with a length that is an integral of the channel count.
|
||||
*
|
||||
* Note: the tapsPerChannel value will be iteratively increased (up to 10x) if a filter cannot be designed to meet
|
||||
* the channel band edge attenuation objective.
|
||||
*
|
||||
* Note: a channelizer filter designed using a Blackman-Harris-7 window and 18-21 taps per channel creates a filter
|
||||
* with attenuation in range of -60 to -80 dB at the edge of the 2x sampled channel.
|
||||
*
|
||||
* @param channelBandwidth - full bandwidth of the channel in hertz
|
||||
* @param channels - number of channels
|
||||
* @param tapsPerChannel - desired filter tap count per channel (may be increased to meet objective)
|
||||
* @param windowType - to use when designing the windowed-sync filter
|
||||
* @param logResults - to log a debug summary of the filter characteristics
|
||||
*
|
||||
* @return filter coefficients
|
||||
*
|
||||
* @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB
|
||||
*/
|
||||
|
||||
/**
|
||||
* Polyphase M2 channelizer sync filter. Designed for an M*channel input and an M2 channel output.
|
||||
*
|
||||
* @param channelBandwidth per channel
|
||||
* @param channels count
|
||||
* @param tapsPerChannel minimum. This may be increased to meet the band edge -6.02dB requirement
|
||||
* @param logResults to log the results of the design
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
|
||||
*/
|
||||
public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel,
|
||||
boolean logResults) throws FilterDesignException
|
||||
{
|
||||
int currentTapsPerChannel = tapsPerChannel;
|
||||
int filterLength = (channels * currentTapsPerChannel) - 1;
|
||||
|
||||
double sampleRate = channelBandwidth * channels;
|
||||
double bandEdge = channelBandwidth / sampleRate;
|
||||
double cutoffFrequency = bandEdge / 2.0;
|
||||
double increment = cutoffFrequency * 0.1;
|
||||
|
||||
//Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff
|
||||
float[] taps = null;
|
||||
|
||||
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
|
||||
double response = FilterFactory.evaluate(taps, bandEdge);
|
||||
|
||||
//Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution
|
||||
double incrementThreshold = 1.0 / sampleRate;
|
||||
|
||||
//Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective
|
||||
while(increment > incrementThreshold)
|
||||
{
|
||||
//If the current cutoff meets the objective, test if a higher cutoff also meets the objective
|
||||
if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR) &&
|
||||
(cutoffFrequency + increment <= bandEdge))
|
||||
{
|
||||
float[] higherCutoffTaps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency + increment, 80.0);
|
||||
double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge);
|
||||
|
||||
if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
|
||||
{
|
||||
cutoffFrequency += increment;
|
||||
taps = higherCutoffTaps;
|
||||
response = higherCutoffResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
increment /= 2.0;
|
||||
}
|
||||
}
|
||||
//If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs
|
||||
else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
|
||||
{
|
||||
increment /= 2.0;
|
||||
}
|
||||
//Decrease the cutoff frequency - we can't meet the objective
|
||||
else
|
||||
{
|
||||
cutoffFrequency -= increment;
|
||||
|
||||
if(cutoffFrequency <= 0)
|
||||
{
|
||||
if(logResults)
|
||||
{
|
||||
mLog.debug("Warning: cannot design channelizer filter with tap count [" + currentTapsPerChannel +
|
||||
"] increasing tap count and starting over");
|
||||
}
|
||||
|
||||
currentTapsPerChannel++;
|
||||
|
||||
//After we've attempted increasing the taps per channel 10x, give up
|
||||
if(currentTapsPerChannel > (tapsPerChannel + 10))
|
||||
{
|
||||
throw new FilterDesignException("Couldn't design filter with taps per channel count in the " +
|
||||
"range of " + tapsPerChannel + " - " + (tapsPerChannel + 10) + " Sample Rate:" + sampleRate + " Channels:" + channels);
|
||||
}
|
||||
|
||||
filterLength = channels * currentTapsPerChannel - 1;
|
||||
cutoffFrequency = channelBandwidth / sampleRate;
|
||||
increment = cutoffFrequency * 0.1;
|
||||
}
|
||||
|
||||
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
|
||||
response = FilterFactory.evaluate(taps, bandEdge);
|
||||
}
|
||||
}
|
||||
|
||||
//This will probably never happen, but ...
|
||||
if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
|
||||
{
|
||||
throw new FilterDesignException("Cannot design filter to specifications");
|
||||
}
|
||||
|
||||
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
|
||||
float[] extendedTaps = new float[taps.length + 1];
|
||||
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
|
||||
|
||||
if(logResults)
|
||||
{
|
||||
mLog.debug("Polyphase Channelizer Filter Design Summary");
|
||||
mLog.debug("-----------------------------------------------------");
|
||||
mLog.debug("Input Sample Rate: " + sampleRate);
|
||||
mLog.debug("Channel Bandwidth: " + channelBandwidth);
|
||||
mLog.debug("Channels: " + channels);
|
||||
mLog.debug("Window Type: " + Window.WindowType.KAISER.name());
|
||||
mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels));
|
||||
mLog.debug("Filter Length: " + (extendedTaps.length));
|
||||
mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge));
|
||||
mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency));
|
||||
mLog.debug("Attenuation at 0.0 OBJECTIVE: " + 0.0);
|
||||
mLog.debug("Attenuation at 1.0 OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE);
|
||||
mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00) + "\tFrequency: " + (sampleRate * bandEdge * 1.0));
|
||||
mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25) + "\tFrequency: " + (sampleRate * bandEdge * 1.25));
|
||||
mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50) + "\tFrequency: " + (sampleRate * bandEdge * 1.5));
|
||||
mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75) + "\tFrequency: " + (sampleRate * bandEdge * 1.75));
|
||||
mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00) + "\tFrequency: " + (sampleRate * bandEdge * 2.0));
|
||||
}
|
||||
|
||||
//Adjust the overall gain of the filter to an objective of 1.0 times the number of channels
|
||||
return normalize(extendedTaps, 1.0f * (float)channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a windowed-sync (Nyquist) filter.
|
||||
*
|
||||
* @param cutoff frequency (0.0 <> 0.5)
|
||||
* @param length of the filter - must be odd-length
|
||||
* @param windowType to use in designing the filter
|
||||
* @return filter coefficients.
|
||||
* @throws FilterDesignException if the requested length is not odd
|
||||
*/
|
||||
public static float[] getSinc(double cutoff, int length, Window.WindowType windowType) throws FilterDesignException
|
||||
{
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
throw new FilterDesignException("Sinc filters must be odd-length");
|
||||
}
|
||||
|
||||
float[] coefficients = new float[length];
|
||||
int half = length / 2;
|
||||
|
||||
double[] window = Window.getWindow(windowType, length);
|
||||
|
||||
double scalor = 2.0 * cutoff;
|
||||
double piScalor = Math.PI * scalor;
|
||||
|
||||
coefficients[half] = (float)(1.0 * scalor * window[half]);
|
||||
|
||||
for(int x = 1; x <= half; x++)
|
||||
{
|
||||
double a = piScalor * x;
|
||||
double coefficient = scalor * Math.sin(a) / a;
|
||||
|
||||
coefficient *= window[half + x];
|
||||
coefficients[half + x] = (float)coefficient;
|
||||
coefficients[half - x] = (float)coefficient;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Nyquist filter using a Kaiser Window.
|
||||
*
|
||||
* @param cutoff frequency (0.0 <> 0.5)
|
||||
* @param length of the filter - must be odd-length
|
||||
* @param attenuation desired for adjacent channels
|
||||
* @return filter coefficients.
|
||||
* @throws FilterDesignException if the requested length is not odd
|
||||
*/
|
||||
public static float[] getKaiserSinc(int length, double cutoff, double attenuation) throws FilterDesignException
|
||||
{
|
||||
if(length % 2 == 0)
|
||||
{
|
||||
throw new FilterDesignException("Sinc filters must be odd-length");
|
||||
}
|
||||
|
||||
float[] coefficients = new float[length];
|
||||
int half = length / 2;
|
||||
|
||||
double[] window = Window.getKaiser(length, attenuation);
|
||||
|
||||
double scalor = 2.0 * cutoff;
|
||||
double piScalor = Math.PI * scalor;
|
||||
|
||||
coefficients[half] = (float)(1.0 * scalor * window[half]);
|
||||
|
||||
for(int x = 1; x <= half; x++)
|
||||
{
|
||||
double a = piScalor * x;
|
||||
double coefficient = scalor * Math.sin(a) / a;
|
||||
|
||||
coefficient *= window[half + x];
|
||||
coefficients[half + x] = (float)coefficient;
|
||||
coefficients[half - x] = (float)coefficient;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two doubles for equals and avoids any rounding error that are present.
|
||||
*
|
||||
* @param a value to compare
|
||||
* @param objective value to compare
|
||||
* @return true if they are equivalent within the margin of error
|
||||
*/
|
||||
private static boolean matchesObjective(double a, double objective, double marginOfError)
|
||||
{
|
||||
return Math.abs(a - objective) <= marginOfError;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
float[] taps = null;
|
||||
|
||||
try
|
||||
{
|
||||
taps = FilterFactory.getSincM2Channelizer(12500.0, 800, 9, true);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error", fde);
|
||||
}
|
||||
|
||||
mLog.debug("Done: " + Arrays.toString(taps));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,234 +1,523 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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/>
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
/*
|
||||
* Kaiser Window design methods: getKaiser(), getKaiserBeta() and getBesselZerothOrder() with minor naming changes were
|
||||
* adopted from:
|
||||
*
|
||||
* https://github.com/rjeschke/neetutils-base/blob/master/src/main/java/com/github/rjeschke/neetutils/audio/FIRUtils.java
|
||||
*
|
||||
* Copyright (C) 2012 René Jeschke <rene_jeschke@yahoo.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Window creator
|
||||
*
|
||||
* Blackman, Hamming and Hanning windows
|
||||
* based on windowing methods described at:
|
||||
* http://www.mstarlabs.com/dsp/goertzel/goertzel.html
|
||||
* and
|
||||
* Understanding Digital Signal Processing 3e, Lyons, p.91
|
||||
* and
|
||||
* Digital Filters for Everyone, Allred, p. 132
|
||||
*
|
||||
* Window factory and corresponding utility methods.
|
||||
*/
|
||||
public class Window
|
||||
{
|
||||
public static double[] getWindow( WindowType type, int length )
|
||||
private final static Logger mLog = LoggerFactory.getLogger(Window.class);
|
||||
|
||||
private static final double TWO_PI = Math.PI * 2.0d;
|
||||
private static final double FOUR_PI = Math.PI * 4.0d;
|
||||
private static final double SIX_PI = Math.PI * 6.0d;
|
||||
private static final double EIGHT_PI = Math.PI * 8.0d;
|
||||
|
||||
/**
|
||||
* Creates a window of the specified type and length.
|
||||
*
|
||||
* @param type of window
|
||||
* @param length of window
|
||||
* @return window of the specified type and length
|
||||
*/
|
||||
public static double[] getWindow(WindowType type, int length)
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case BLACKMAN:
|
||||
return getBlackmanWindow( length );
|
||||
case COSINE:
|
||||
return getCosineWindow( length );
|
||||
case HAMMING:
|
||||
return getHammingWindow( length );
|
||||
case HANNING:
|
||||
return getHanningWindow( length );
|
||||
case NONE:
|
||||
default:
|
||||
return getRectangularWindow( length );
|
||||
}
|
||||
switch(type)
|
||||
{
|
||||
case BLACKMAN:
|
||||
return getBlackman(length);
|
||||
case BLACKMAN_HARRIS_4:
|
||||
return getBlackmanHarris4(length);
|
||||
case BLACKMAN_HARRIS_7:
|
||||
return getBlackmanHarris7(length);
|
||||
case BLACKMAN_NUTALL:
|
||||
return getBlackmanNutall(length);
|
||||
case COSINE:
|
||||
return getCosine(length);
|
||||
case FLAT_TOP:
|
||||
return getFlatTop(length);
|
||||
case HAMMING:
|
||||
return getHamming(length);
|
||||
case HANN:
|
||||
return getHann(length);
|
||||
case KAISER:
|
||||
throw new IllegalArgumentException("Kaiser Window cannot be created via this method. Use the getKaiser() method instead.");
|
||||
case NUTALL:
|
||||
return getNutall(length);
|
||||
case NONE:
|
||||
default:
|
||||
return getRectangular(length);
|
||||
}
|
||||
}
|
||||
|
||||
public static double[] getRectangularWindow( int length )
|
||||
/**
|
||||
* Creates an all-pass or rectangular window.
|
||||
*
|
||||
* @param length of the window
|
||||
* @return rectangular window
|
||||
*/
|
||||
public static double[] getRectangular(int length)
|
||||
{
|
||||
double[] coefficients = new double[ length ];
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
Arrays.fill( coefficients, 1.0d );
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
public static double[] getCosineWindow( int length )
|
||||
{
|
||||
double[] coefficients = new double[ length ];
|
||||
Arrays.fill(coefficients, 1.0d);
|
||||
|
||||
|
||||
if( length % 2 == 0 ) //Even length
|
||||
{
|
||||
int half = (int)( ( length - 1 ) / 2 );
|
||||
|
||||
for( int x = -half; x < length / 2 + 1; x++ )
|
||||
{
|
||||
coefficients[ x + half ] = Math.cos(
|
||||
( (double)x * Math.PI ) / ( (double)length + 1.0d ) );
|
||||
}
|
||||
}
|
||||
else //Odd length
|
||||
{
|
||||
int half = (int) length / 2;
|
||||
|
||||
for( int x = -half; x < half + 1; x++ )
|
||||
{
|
||||
coefficients[ x + half ] = Math.cos(
|
||||
( (double)x * Math.PI ) / ( (double)length + 1.0d ) );
|
||||
}
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
public static double[] getBlackmanWindow( int length )
|
||||
/**
|
||||
* Creates a cosine window of the specified length.
|
||||
*
|
||||
* @param length of the window
|
||||
* @return cosine window
|
||||
*/
|
||||
public static double[] getCosine(int length)
|
||||
{
|
||||
double[] coefficients = new double[ length ];
|
||||
|
||||
for( int x = 0; x < length; x++ )
|
||||
{
|
||||
coefficients[ x ] = .426591D -
|
||||
( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) +
|
||||
( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) );
|
||||
}
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
public static double[] getHammingWindow( int length )
|
||||
{
|
||||
double[] coefficients = new double[ length ];
|
||||
|
||||
if( length % 2 == 0 ) //Even length
|
||||
{
|
||||
for( int x = 0; x < length; x++ )
|
||||
{
|
||||
coefficients[ x ] = .54D - ( .46D *
|
||||
Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) );
|
||||
}
|
||||
}
|
||||
else //Odd length
|
||||
{
|
||||
for( int x = 0; x < length; x++ )
|
||||
{
|
||||
coefficients[ x ] = .54D - ( .46D *
|
||||
Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) );
|
||||
}
|
||||
}
|
||||
if(length % 2 == 0) //Even length
|
||||
{
|
||||
int half = (int)((length - 1) / 2);
|
||||
|
||||
return coefficients;
|
||||
for(int x = -half; x < length / 2 + 1; x++)
|
||||
{
|
||||
coefficients[x + half] = Math.cos(
|
||||
((double)x * Math.PI) / ((double)length + 1.0d));
|
||||
}
|
||||
}
|
||||
else //Odd length
|
||||
{
|
||||
int half = (int)length / 2;
|
||||
|
||||
for(int x = -half; x < half + 1; x++)
|
||||
{
|
||||
coefficients[x + half] = Math.cos(
|
||||
((double)x * Math.PI) / ((double)length + 1.0d));
|
||||
}
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
public static double[] getHanningWindow( int length )
|
||||
/**
|
||||
* Creates a 3-term Blackman window.
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Blackman window
|
||||
*/
|
||||
public static double[] getBlackman(int length)
|
||||
{
|
||||
double[] coefficients = new double[ length ];
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
if( length % 2 == 0 ) //Even length
|
||||
{
|
||||
for( int x = 0; x < length; x++ )
|
||||
{
|
||||
coefficients[ x ] = .5D - ( .5D *
|
||||
Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) );
|
||||
}
|
||||
}
|
||||
else //Odd length
|
||||
{
|
||||
for( int x = 0; x < length; x++ )
|
||||
{
|
||||
coefficients[ x ] = .5D - ( .5D *
|
||||
Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) );
|
||||
}
|
||||
}
|
||||
double denominator = length - 1;
|
||||
|
||||
double a0 = 0.426590713672;
|
||||
double a1 = 0.496560619089;
|
||||
double a2 = 0.0768486672399;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 -
|
||||
(a1 * Math.cos((TWO_PI * (double)x) / denominator)) +
|
||||
(a2 * Math.cos((FOUR_PI * (double)x) / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 4-term Nutall window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Nutall window
|
||||
*/
|
||||
public static double[] getNutall(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double denominator = length - 1;
|
||||
double a0 = 0.355768;
|
||||
double a1 = 0.487396;
|
||||
double a2 = 0.144232;
|
||||
double a3 = 0.012604;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 -
|
||||
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
|
||||
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
|
||||
(a3 * Math.cos(SIX_PI * (double)x / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 4-term Blackman-Nutall window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Blackman-Nutall window
|
||||
*/
|
||||
public static double[] getBlackmanNutall(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double denominator = length - 1;
|
||||
double a0 = 0.3635819;
|
||||
double a1 = 0.4891775;
|
||||
double a2 = 0.1365995;
|
||||
double a3 = 0.0106411;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 -
|
||||
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
|
||||
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
|
||||
(a3 * Math.cos(SIX_PI * (double)x / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 4-term Blackman-Harris window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Blackman-Harris window
|
||||
*/
|
||||
public static double[] getBlackmanHarris4(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double denominator = length - 1;
|
||||
double a0 = 0.35875;
|
||||
double a1 = 0.48829;
|
||||
double a2 = 0.14128;
|
||||
double a3 = 0.01168;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 -
|
||||
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
|
||||
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
|
||||
(a3 * Math.cos(SIX_PI * (double)x / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 7-term Blackman-Harris window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Blackman-Harris window
|
||||
*/
|
||||
public static double[] getBlackmanHarris7(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double denominator = length - 1;
|
||||
|
||||
double a0 = 0.27105140069342;
|
||||
double a1 = 0.43329793923448;
|
||||
double a2 = 0.21812299954311;
|
||||
double a3 = 0.06592544638803;
|
||||
double a4 = 0.01081174209837;
|
||||
double a5 = 0.00077658482522;
|
||||
double a6 = 0.00001388721735;
|
||||
|
||||
|
||||
return coefficients;
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
double w = TWO_PI * (double)x / denominator;
|
||||
|
||||
coefficients[x] = a0 -
|
||||
(a1 * Math.cos(w)) +
|
||||
(a2 * Math.cos(2.0 * w)) -
|
||||
(a3 * Math.cos(3.0 * w)) +
|
||||
(a4 * Math.cos(4.0 * w)) -
|
||||
(a5 * Math.cos(5.0 * w)) +
|
||||
(a6 * Math.cos(6.0 * w));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Hamming window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Hamming window
|
||||
*/
|
||||
public static double[] getHamming(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double a0 = 0.54;
|
||||
double a1 = 0.46;
|
||||
double denominator = length;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Hann window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Hann window
|
||||
*/
|
||||
public static double[] getHann(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double a0 = 0.5;
|
||||
double a1 = 0.5;
|
||||
double denominator = length;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 4-term Blackman-Harris window
|
||||
*
|
||||
* @param length of the window
|
||||
* @return Blackman-Harris window
|
||||
*/
|
||||
public static double[] getFlatTop(int length)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double denominator = length - 1;
|
||||
|
||||
double a0 = 0.215578948;
|
||||
double a1 = 0.41663158;
|
||||
double a2 = 0.277263158;
|
||||
double a3 = 0.083578947;
|
||||
double a4 = 0.006947368;
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
coefficients[x] = a0 -
|
||||
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
|
||||
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
|
||||
(a3 * Math.cos(SIX_PI * (double)x / denominator)) +
|
||||
(a4 * Math.cos(EIGHT_PI * (double)x / denominator));
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the beta (roll-off factor) for a Kaiser window based on the specified attenuation.
|
||||
*
|
||||
* @param attenuation in decibels
|
||||
* @return beta value in the range of 0.0 to 1.0
|
||||
*/
|
||||
public static double getKaiserBeta(double attenuation)
|
||||
{
|
||||
if(attenuation > 50.0)
|
||||
{
|
||||
return 0.1102 * (attenuation - 8.7);
|
||||
}
|
||||
else if(attenuation >= 21.0)
|
||||
{
|
||||
return (0.5842 * Math.pow(attenuation - 21.0, 0.4)) + (0.07886 * (attenuation - 21.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Kaiser window.
|
||||
*
|
||||
* @param length of the window
|
||||
* @param attenuation desired
|
||||
* @return window
|
||||
*/
|
||||
public static double[] getKaiser(int length, double attenuation)
|
||||
{
|
||||
double[] coefficients = new double[length];
|
||||
|
||||
double beta = getKaiserBeta(attenuation);
|
||||
|
||||
double betaBesselZerothOrder = getBesselZerothOrder(beta);
|
||||
|
||||
double temp;
|
||||
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
temp = beta * Math.sqrt(1.0 - Math.pow(2.0 * x / (length - 1) - 1.0, 2));
|
||||
coefficients[x] = getBesselZerothOrder(temp) / betaBesselZerothOrder;
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Zeroth order modified Bessel function.
|
||||
*
|
||||
* @param x Value.
|
||||
* @return Return value.
|
||||
*/
|
||||
public final static double getBesselZerothOrder(final double x)
|
||||
{
|
||||
double f = 1;
|
||||
final double x2 = x * x * 0.25;
|
||||
double xc = x2;
|
||||
double v = 1 + x2;
|
||||
for (int i = 2; i < 100; i++)
|
||||
{
|
||||
f *= i;
|
||||
xc *= x2;
|
||||
final double a = xc / (f * f);
|
||||
v += a;
|
||||
if (a < 1e-20) break;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the window against an array of float-type samples
|
||||
*/
|
||||
public static float[] apply( double[] coefficients, float[] samples )
|
||||
public static float[] apply(double[] coefficients, float[] samples)
|
||||
{
|
||||
for( int x = 0; x < coefficients.length; x++ )
|
||||
{
|
||||
samples[ x ] = (float)( samples[ x ] * coefficients[ x ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public static float[] apply( WindowType type, float[] samples )
|
||||
{
|
||||
double[] coefficients = getWindow( type, samples.length );
|
||||
|
||||
return apply( coefficients, samples );
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the window against an array of float-type samples
|
||||
*/
|
||||
public static double[] apply( double[] coefficients, double[] samples )
|
||||
{
|
||||
for( int x = 0; x < coefficients.length; x++ )
|
||||
{
|
||||
samples[ x ] = samples[ x ] * coefficients[ x ];
|
||||
}
|
||||
|
||||
return samples;
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
samples[x] = (float)(samples[x] * coefficients[x]);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public static double[] apply( WindowType type, double[] samples )
|
||||
/**
|
||||
* Apply the window type against the float array of samples
|
||||
*
|
||||
* @param type of window to use
|
||||
* @param samples to be windowed
|
||||
* @return windowed samples
|
||||
*/
|
||||
public static float[] apply(WindowType type, float[] samples)
|
||||
{
|
||||
double[] coefficients = getWindow( type, samples.length );
|
||||
|
||||
return apply( coefficients, samples );
|
||||
double[] coefficients = getWindow(type, samples.length);
|
||||
|
||||
return apply(coefficients, samples);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Apply the window against an array of double-type samples
|
||||
*/
|
||||
public static double[] apply(double[] coefficients, double[] samples)
|
||||
{
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
samples[x] = samples[x] * coefficients[x];
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the window type against the double array of samples
|
||||
*
|
||||
* @param type of window to use
|
||||
* @param samples to be windowed
|
||||
* @return windowed samples
|
||||
*/
|
||||
public static double[] apply(WindowType type, double[] samples)
|
||||
{
|
||||
double[] coefficients = getWindow(type, samples.length);
|
||||
|
||||
return apply(coefficients, samples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Window types
|
||||
*/
|
||||
public enum WindowType
|
||||
{
|
||||
BLACKMAN, COSINE, HAMMING, HANNING, NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to log the double arrays
|
||||
*/
|
||||
public static String arrayToString( double[] array, boolean breaks )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for( int x = 0; x < array.length; x++ )
|
||||
{
|
||||
sb.append( x + ":" + array[ x ] );
|
||||
BLACKMAN("Blackman"),
|
||||
BLACKMAN_HARRIS_4("Blackman-Harris 4"),
|
||||
BLACKMAN_HARRIS_7("Blackman-Harris 7"),
|
||||
BLACKMAN_NUTALL("Blackman-Nutall"),
|
||||
COSINE("Cosine"),
|
||||
FLAT_TOP("Flat Top"),
|
||||
HAMMING("Hamming"),
|
||||
HANN("Hann"),
|
||||
KAISER("Kaiser"),
|
||||
NUTALL("Nutall"),
|
||||
NONE("None");
|
||||
|
||||
if( breaks )
|
||||
{
|
||||
sb.append( "\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( x % 8 == 7 )
|
||||
{
|
||||
sb.append( "\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "\t" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
private String mLabel;
|
||||
|
||||
WindowType(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
double[] kaiser = getKaiser(16, 80.0);
|
||||
|
||||
mLog.debug("Window:" + Arrays.toString(kaiser));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBufferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public abstract class AbstractComplexPolyphaseChannelizer implements Listener<ReusableComplexBuffer>, ISourceEventListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class);
|
||||
private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue("AbstractComplexPolyphaseChannelizer");
|
||||
private Broadcaster<SourceEvent> mSourceChangeBroadcaster = new Broadcaster();
|
||||
private List<PolyphaseChannelSource> mChannels = new CopyOnWriteArrayList<>();
|
||||
private double mSampleRate;
|
||||
private int mChannelCount;
|
||||
private int mSubChannelCount;
|
||||
private double mChannelSampleRate;
|
||||
|
||||
/**
|
||||
* Complex sample polyphase channelizer
|
||||
*
|
||||
* @param channelCount
|
||||
*/
|
||||
public AbstractComplexPolyphaseChannelizer(double sampleRate, int channelCount)
|
||||
{
|
||||
mChannelCount = channelCount;
|
||||
mSubChannelCount = channelCount * 2; //Number of I/Q channels
|
||||
mSampleRate = sampleRate;
|
||||
mChannelSampleRate = (double)mSampleRate / (double)mChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input sample rate for this channelizer
|
||||
* @return sample rate in hertz
|
||||
*/
|
||||
public double getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input sample rate for for this channelizer
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
public void setRates(double sampleRate, int channelCount)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
mChannelCount = channelCount;
|
||||
mChannelSampleRate = mSampleRate / (double)mChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of channels being processed by this channelizer.
|
||||
*/
|
||||
public int getChannelCount()
|
||||
{
|
||||
return mChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of sub channels (I/Q) being processed by this channelizer. This is 2 * channel count.
|
||||
*/
|
||||
public int getSubChannelCount()
|
||||
{
|
||||
return mSubChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output channel sample rate
|
||||
* @return sample rate in hertz
|
||||
*/
|
||||
public double getChannelSampleRate()
|
||||
{
|
||||
return mChannelSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the processed channel samples to any registered polyphase channel outputs.
|
||||
*
|
||||
* @param channelResultsBuffer containing an array of an array of I/Q samples per channel
|
||||
*/
|
||||
protected void dispatch(ReusableChannelResultsBuffer channelResultsBuffer)
|
||||
{
|
||||
for(PolyphaseChannelSource channel : mChannels)
|
||||
{
|
||||
channelResultsBuffer.incrementUserCount();
|
||||
channel.receiveChannelResults(channelResultsBuffer);
|
||||
}
|
||||
|
||||
channelResultsBuffer.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the polyphase channel source to receive processed output channel samples
|
||||
*
|
||||
* @param polyphaseChannelSource
|
||||
*/
|
||||
public void addChannel(PolyphaseChannelSource polyphaseChannelSource)
|
||||
{
|
||||
if(polyphaseChannelSource != null && !mChannels.contains(polyphaseChannelSource))
|
||||
{
|
||||
mChannels.add(polyphaseChannelSource);
|
||||
mSourceChangeBroadcaster.addListener(polyphaseChannelSource.getSourceEventListener());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the polyphase channel source from receiving output channel samples.
|
||||
*
|
||||
* @param polyphaseChannelSource
|
||||
*/
|
||||
public void removeChannel(PolyphaseChannelSource polyphaseChannelSource)
|
||||
{
|
||||
if(polyphaseChannelSource != null && mChannels.contains(polyphaseChannelSource))
|
||||
{
|
||||
mChannels.remove(polyphaseChannelSource);
|
||||
mSourceChangeBroadcaster.removeListener(polyphaseChannelSource.getSourceEventListener());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of polyphase channels registered to receive sample streams
|
||||
*/
|
||||
public int getRegisteredChannelCount()
|
||||
{
|
||||
return mChannels.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or reuses a channel results buffer for subclass implementations to use for temporary
|
||||
* storage of channel results through distribution and consumption of channel results by
|
||||
* channel output processors.
|
||||
*/
|
||||
protected ReusableChannelResultsBuffer getChannelResultsBuffer()
|
||||
{
|
||||
return mBufferQueue.getBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceChangeBroadcaster;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
/*******************************************************************************
|
||||
* *********************************************************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ChannelCalculator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class);
|
||||
|
||||
/**
|
||||
* Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency
|
||||
* from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle
|
||||
* of the wrap-around (N/2) index.
|
||||
*/
|
||||
public enum IndexBoundaryPolicy
|
||||
{
|
||||
ADJUST_POSITIVE,
|
||||
ADJUST_NEGATIVE
|
||||
}
|
||||
|
||||
private double mSampleRate;
|
||||
private int mChannelCount;
|
||||
private double mCenterFrequency;
|
||||
private double mChannelBandwidth;
|
||||
private double mOversampling = 2.0;
|
||||
|
||||
/**
|
||||
* Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source
|
||||
* with a specified channel center frequency and bandwidth.
|
||||
*
|
||||
* Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their
|
||||
* center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods
|
||||
* to determine the min/center/max frequency of each channel that is adjusted for the current center frequency
|
||||
* value and to determine the channel index(es) that need to be targeted to produce a requested output channel
|
||||
* with a specific frequency and bandwdith.
|
||||
*
|
||||
* @param sampleRate in hertz for the input sample rate from the sample source (ie tuner)
|
||||
* @param channelCount number of channels in the polyphase filter bank
|
||||
* @param centerFrequency center frequency for the input sample stream
|
||||
* @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling
|
||||
*/
|
||||
public ChannelCalculator(double sampleRate, int channelCount, double centerFrequency, double oversampling)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
mChannelCount = channelCount;
|
||||
mCenterFrequency = centerFrequency;
|
||||
mOversampling = oversampling;
|
||||
|
||||
updateChannelBandwidth();;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the channel bandwidth from the sample rate and channel count.
|
||||
*/
|
||||
private void updateChannelBandwidth()
|
||||
{
|
||||
mChannelBandwidth = (double)mSampleRate / (double)mChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Center frequency for the channelizer
|
||||
*/
|
||||
public double getCenterFrequency()
|
||||
{
|
||||
return mCenterFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the center frequency for the channelizer
|
||||
*/
|
||||
public void setCenterFrequency(double frequency)
|
||||
{
|
||||
mCenterFrequency = frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of channels being channelized by the polyphase channelizer
|
||||
*/
|
||||
public int getChannelCount()
|
||||
{
|
||||
return mChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the number of channels being channelized by the polyphase channelizer
|
||||
*/
|
||||
public void setChannelCount(int channelCount)
|
||||
{
|
||||
mChannelCount = channelCount;
|
||||
updateChannelBandwidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper
|
||||
* end of the spectrum and the upper half of the channel exists at the lower end of the spectrum
|
||||
*
|
||||
* @return wrap-around channel index
|
||||
*/
|
||||
public int getWrapAroundIndex()
|
||||
{
|
||||
return mChannelCount / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* PolyphaseChannelManager input sample rate
|
||||
* @return input sample rate in hertz
|
||||
*/
|
||||
public double getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the channel count using the sample rate argument to calculate the number of channels from the
|
||||
* already established channel bandwidth.
|
||||
*
|
||||
* @param sampleRate to use for channel count
|
||||
*/
|
||||
public void setRates(double sampleRate, int channelCount)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
mChannelCount = channelCount;
|
||||
updateChannelBandwidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling
|
||||
* is being used.
|
||||
*/
|
||||
public double getChannelBandwidth()
|
||||
{
|
||||
return mChannelBandwidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Half of the channel bandwidth.
|
||||
*/
|
||||
public double getHalfChannelBandwidth()
|
||||
{
|
||||
return mChannelBandwidth / 2.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Oversampling rate produced by the channelizer.
|
||||
*
|
||||
* @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied.
|
||||
*/
|
||||
public double getOversampling()
|
||||
{
|
||||
return mOversampling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the oversampling rate being applied by the polyphase channelizer
|
||||
* @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates
|
||||
* that the polyphase channelizer is oversampling each channel.
|
||||
*/
|
||||
public void setOversampling(double oversampling)
|
||||
{
|
||||
mOversampling = oversampling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel sample rate. This value is the product of the filtered channel bandwidth times the
|
||||
* oversampling rate.
|
||||
*/
|
||||
public double getChannelSampleRate()
|
||||
{
|
||||
return getChannelBandwidth() * getOversampling();
|
||||
}
|
||||
|
||||
/**
|
||||
* PolyphaseChannelManager input minimum frequency
|
||||
* @return minimum frequency in hertz
|
||||
*/
|
||||
public long getMinimumFrequency()
|
||||
{
|
||||
return (long)(getCenterFrequency() - (getSampleRate() / 2.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* PolyphaseChannelManager input maximum frequency
|
||||
* @return maximum frequency in hertz
|
||||
*/
|
||||
public long getMaximumFrequency()
|
||||
{
|
||||
return (long)(getCenterFrequency() + (getSampleRate() / 2.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center
|
||||
* frequency and channel bandwidth.
|
||||
*
|
||||
* @param tunerChannel specifying the desired channel frequency and bandwidth
|
||||
*
|
||||
* @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel
|
||||
* for the desired frequency and bandwidth.
|
||||
*
|
||||
* @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained
|
||||
* within the channelizer's minimum or maximum frequency range
|
||||
*/
|
||||
public List<Integer> getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException
|
||||
{
|
||||
if(tunerChannel.getMinFrequency() < getMinimumFrequency() ||
|
||||
tunerChannel.getMaxFrequency() > getMaximumFrequency())
|
||||
{
|
||||
throw new IllegalArgumentException("Requested channel cannot be provided by this " +
|
||||
"channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " +
|
||||
tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" +
|
||||
getMinimumFrequency() + " - " + getMaximumFrequency() + "]");
|
||||
}
|
||||
|
||||
int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
|
||||
//Detect the unsourceable situation where the wrap around index would be included in both the minimum index
|
||||
//and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate.
|
||||
if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex())
|
||||
{
|
||||
throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " +
|
||||
"within two channel bandwidths of the sample rate.");
|
||||
}
|
||||
|
||||
List<Integer> indexes = new ArrayList<>();
|
||||
|
||||
indexes.add(minIndex);
|
||||
|
||||
if(minIndex != maxIndex)
|
||||
{
|
||||
//Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below
|
||||
if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " +
|
||||
"channel indexes for tuner channel: " + tunerChannel);
|
||||
}
|
||||
|
||||
int pointer = minIndex + 1;
|
||||
|
||||
if(pointer >= getChannelCount())
|
||||
{
|
||||
pointer -= getChannelCount();
|
||||
}
|
||||
|
||||
while(pointer != maxIndex)
|
||||
{
|
||||
indexes.add(pointer);
|
||||
|
||||
pointer++;
|
||||
|
||||
if(pointer >= getChannelCount())
|
||||
{
|
||||
pointer -= getChannelCount();
|
||||
}
|
||||
}
|
||||
|
||||
indexes.add(maxIndex);
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency
|
||||
* of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your
|
||||
* choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel
|
||||
* index (ADJUST_NEGATIVE).
|
||||
*
|
||||
* @param frequency to determine channel index for
|
||||
* @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries
|
||||
* @return index for the frequency
|
||||
*/
|
||||
public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
double offset = frequency - getCenterFrequency();
|
||||
|
||||
if(Math.abs(offset) < getHalfChannelBandwidth())
|
||||
{
|
||||
return 0; //Center index
|
||||
}
|
||||
|
||||
if(offset > 0)
|
||||
{
|
||||
offset += getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
offset -= getHalfChannelBandwidth();
|
||||
}
|
||||
|
||||
int indexOffset = (int)(offset / getChannelBandwidth());
|
||||
|
||||
if(indexOffset < 0)
|
||||
{
|
||||
indexOffset += getChannelCount();
|
||||
}
|
||||
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE &&
|
||||
isOverlapFrequency(frequency, indexOffset, indexOffset + 1))
|
||||
{
|
||||
indexOffset = normalize(indexOffset + 1);
|
||||
}
|
||||
else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE &&
|
||||
isOverlapFrequency(frequency, indexOffset - 1, indexOffset))
|
||||
{
|
||||
indexOffset = normalize(indexOffset - 1);
|
||||
}
|
||||
|
||||
return indexOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the
|
||||
* maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are
|
||||
* the same as the frequency argument.
|
||||
*
|
||||
* Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap
|
||||
*
|
||||
* @param frequency to test for overlap between index 1 and 2
|
||||
* @param index1 which is the lower channel index located next to index 2.
|
||||
* @param index2 which is the higher channel index located next to index 1.
|
||||
* @return true if frequency = index1 maximum frequency = index2 minimum frequency
|
||||
*/
|
||||
public boolean isOverlapFrequency(long frequency, int index1, int index2)
|
||||
{
|
||||
index1 = normalize(index1);
|
||||
index2 = normalize(index2);
|
||||
|
||||
//Check that these two indexes are side by side
|
||||
int delta = normalize(index2 - index1);
|
||||
|
||||
if(delta != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
|
||||
if(index1 == getWrapAroundIndex())
|
||||
{
|
||||
index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
}
|
||||
|
||||
if(index2 == getWrapAroundIndex())
|
||||
{
|
||||
index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
}
|
||||
|
||||
return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the index to the range of 0 <> N-1 channel indexes</>
|
||||
*/
|
||||
private int normalize(int index)
|
||||
{
|
||||
while(index < 0)
|
||||
{
|
||||
index += getChannelCount();
|
||||
}
|
||||
|
||||
while(index >= getChannelCount())
|
||||
{
|
||||
index -= getChannelCount();
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the center frequency for the channel identified by the index in the channel results array.
|
||||
*
|
||||
* @param index to determine the channel center frequency
|
||||
* @return center frequency for the indexed channel
|
||||
*/
|
||||
public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Illegal channel index [" + index + "] current max channels[" +
|
||||
getChannelCount() + "]");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
|
||||
if(index == wrapAroundIndex)
|
||||
{
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
|
||||
{
|
||||
return getCenterFrequency() + (index * getChannelBandwidth());
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - (index * getChannelBandwidth());
|
||||
}
|
||||
}
|
||||
else if(index < wrapAroundIndex)
|
||||
{
|
||||
return getCenterFrequency() + (index * getChannelBandwidth());
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum frequency band edge for the channel identified by the index in the channel results
|
||||
* array.
|
||||
* @param index to calculate the minimum band edge frequency for the channel.
|
||||
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum
|
||||
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the
|
||||
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
|
||||
* @return frequency of the minimum band edge
|
||||
*/
|
||||
public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" +
|
||||
index + "]");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
|
||||
if(index == wrapAroundIndex)
|
||||
{
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
|
||||
{
|
||||
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getIndexCenterFrequency(index, indexBoundaryPolicy);
|
||||
}
|
||||
}
|
||||
else if(index <= wrapAroundIndex)
|
||||
{
|
||||
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) -
|
||||
getHalfChannelBandwidth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum frequency band edge for the channel identified by the index in the channel results
|
||||
* array.
|
||||
* @param index to calculate the maximum band edge frequency for the channel.
|
||||
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum
|
||||
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the
|
||||
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
|
||||
* @return frequency of the maximum band edge
|
||||
*/
|
||||
public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
|
||||
{
|
||||
if(index < 0 || index >= getChannelCount())
|
||||
{
|
||||
throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" +
|
||||
index + "]");
|
||||
}
|
||||
|
||||
int wrapAroundIndex = getWrapAroundIndex();
|
||||
|
||||
if(index == wrapAroundIndex)
|
||||
{
|
||||
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
|
||||
{
|
||||
return getIndexCenterFrequency(index, indexBoundaryPolicy);
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth());
|
||||
}
|
||||
}
|
||||
else if(index <= wrapAroundIndex)
|
||||
{
|
||||
return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) +
|
||||
getHalfChannelBandwidth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the center frequency for a channel that will be synthesized from the channel indexes.
|
||||
* @param indexes that are contiguous in the spectrum
|
||||
* @return center frequency in hertz
|
||||
*/
|
||||
public long getCenterFrequencyForIndexes(List<Integer> indexes)
|
||||
{
|
||||
if(indexes.isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("Indexes cannot be empty");
|
||||
}
|
||||
|
||||
int centerIndex = (indexes.size() - 1) / 2;
|
||||
|
||||
int index = indexes.get(centerIndex);
|
||||
|
||||
if(indexes.size() % 2 == 0)
|
||||
{
|
||||
return (long)getIndexMaximumFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(index == getWrapAroundIndex())
|
||||
{
|
||||
return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_POSITIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation of the channel calculator settings
|
||||
*/
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Channel Calculator Settings");
|
||||
sb.append(" Channels:").append(getChannelCount());
|
||||
sb.append(" Sample Rate:").append(getSampleRate());
|
||||
sb.append(" Channel Bandwidth:").append(getChannelBandwidth());
|
||||
sb.append(" Channel Rate:").append(getChannelSampleRate());
|
||||
sb.append(" Min:").append(getMinimumFrequency());
|
||||
sb.append(" Max:").append(getMaximumFrequency());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.IOverflowListener;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
|
||||
* stream into equal bandwidth channels that are each oversampled by 2x for output.
|
||||
*
|
||||
* This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal
|
||||
* Processing for Communications Systems, p230-233.
|
||||
*
|
||||
* Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated
|
||||
* to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine
|
||||
* shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage
|
||||
* native processor intrinsics for efficiency.
|
||||
*
|
||||
* The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer.
|
||||
*
|
||||
* Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer
|
||||
* are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction
|
||||
* Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into four steps:
|
||||
*
|
||||
* -Multiply the inline array of samples and filter coefficients
|
||||
* -Accumulate the results for each sub-channel
|
||||
* -Rearrange the sub-channel results to correctly order the sub-channels
|
||||
* -Perform IFFT
|
||||
*
|
||||
* Note: design the prototype filter as a Nyquist windowed filter with a -6.02 db attenuation at the channel edge
|
||||
* frequency if you need Perfect Reconstruction where you'll later re-join two or more channels to form a wider
|
||||
* bandwidth channel or to isolate a signal that located between two channels.
|
||||
*/
|
||||
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
|
||||
|
||||
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 12500;
|
||||
|
||||
//Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers
|
||||
private IFFTProcessor mIFFTProcessor = new IFFTProcessor((5 * 152), (2 * 152));
|
||||
private FloatFFT_1D mFFT;
|
||||
private float[] mInlineSamples;
|
||||
private float[] mInlineFilter;
|
||||
private float[] mInlineInterimOutput;
|
||||
private float[] mFilterAccumulator;
|
||||
private boolean mTopBlockIndicator = true;
|
||||
private int[] mTopBlockMap;
|
||||
private int[] mMiddleBlockMap;
|
||||
private int mSampleBufferPointer;
|
||||
private int mSamplesPerBlock;
|
||||
private int mTapsPerChannel;
|
||||
|
||||
/**
|
||||
* Creates a NMDPFB channelizer instance.
|
||||
*
|
||||
* @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency
|
||||
* equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more
|
||||
* channel outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed
|
||||
* as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency
|
||||
* @param sampleRate of the incoming sample stream
|
||||
* @param channelCount - number of filters/channels to output. Since this filter bank performs 2x oversampling for
|
||||
* each channel output, this number must be even (divisible by 2).
|
||||
*/
|
||||
public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount)
|
||||
{
|
||||
super(sampleRate, channelCount);
|
||||
|
||||
if(channelCount % 2 != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Channel count must be an even multiple of the over-sample rate (2x)");
|
||||
}
|
||||
|
||||
mTapsPerChannel = (int)Math.ceil((double)taps.length / (double)channelCount);
|
||||
|
||||
init(taps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a NMDPFB channelizer instance and designs a Perfect Reconstruction prototype filter appropriate for
|
||||
* the baseband sample rate and quantity of filter taps per polyphase sub-channel.
|
||||
*
|
||||
* @param sampleRate to be channelized.
|
||||
* @param tapsPerChannel to use when designing the filter
|
||||
*/
|
||||
public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerChannel) throws FilterDesignException
|
||||
{
|
||||
super(sampleRate, getChannelCount(sampleRate));
|
||||
|
||||
mTapsPerChannel = tapsPerChannel;
|
||||
|
||||
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
|
||||
mTapsPerChannel, false);
|
||||
|
||||
init(filterTaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts sample processing
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
mIFFTProcessor.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops sample processing.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
mIFFTProcessor.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the multiple of two number of channels that can be channelized from the specified sample rate so that
|
||||
* each channel has a minimum bandwidth of the default channel bandwidth (12.5 kHz).
|
||||
* @param sampleRate to channelize
|
||||
* @return number of multiple of two channels that can be channelized.
|
||||
*/
|
||||
public static int getChannelCount(double sampleRate)
|
||||
{
|
||||
int channels = (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH);
|
||||
|
||||
if(channels % 2 != 0)
|
||||
{
|
||||
channels--;
|
||||
}
|
||||
|
||||
mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each");
|
||||
return channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the
|
||||
* sample rate and reinitializes all internal data structures to prepare for processing the new sample rate.
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
@Override
|
||||
public void setRates(double sampleRate, int channelCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
super.setRates(sampleRate, channelCount);
|
||||
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
|
||||
mTapsPerChannel, false);
|
||||
|
||||
init(filterTaps);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
throw new IllegalArgumentException("Cannot create a channelizer filter for the specified sample rate [" +
|
||||
sampleRate + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the complex sample buffer and processes the results through the channelizer.
|
||||
*/
|
||||
@Override
|
||||
public void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
{
|
||||
ReusableChannelResultsBuffer channelResultsBuffer = getChannelResultsBuffer();
|
||||
channelResultsBuffer.setTimestamp(reusableComplexBuffer.getTimestamp());
|
||||
|
||||
float[] samples = reusableComplexBuffer.getSamples();
|
||||
|
||||
int samplesPointer = 0;
|
||||
int samplesToCopy;
|
||||
|
||||
while(samplesPointer < samples.length)
|
||||
{
|
||||
if(mSampleBufferPointer < mSamplesPerBlock)
|
||||
{
|
||||
samplesToCopy = mSamplesPerBlock - mSampleBufferPointer;
|
||||
|
||||
if((samples.length - samplesPointer) < samplesToCopy)
|
||||
{
|
||||
samplesToCopy = (samples.length - samplesPointer);
|
||||
}
|
||||
|
||||
System.arraycopy(samples, samplesPointer, mInlineSamples, mSampleBufferPointer, samplesToCopy);
|
||||
|
||||
mSampleBufferPointer += samplesToCopy;
|
||||
samplesPointer += samplesToCopy;
|
||||
}
|
||||
|
||||
if(mSampleBufferPointer >= mSamplesPerBlock)
|
||||
{
|
||||
//Filter buffered samples and produce a single sample across each of the polyphase channels
|
||||
process(channelResultsBuffer);
|
||||
|
||||
//Right-shift the samples in the buffer over to make room for a new block of samples
|
||||
//Note: since JDK 8, hotspot JIT compiler uses native processor intrinsics for efficiency
|
||||
System.arraycopy(mInlineSamples, 0, mInlineSamples, mSamplesPerBlock, (mInlineSamples.length - mSamplesPerBlock));
|
||||
mSampleBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//Enqueue the channel results buffer for IFFT processing and distribution on a different thread
|
||||
mIFFTProcessor.receive(channelResultsBuffer);
|
||||
|
||||
//Decrement the user count to let the originator know we're done with their buffer
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a top-block processing accumulator map that maps each interim filter and sample index product
|
||||
* to the corresponding final output index for the array that will feed the IFFT.
|
||||
*
|
||||
* @param channelCount - number of channels
|
||||
* @return output index to filter accumulator index mapping
|
||||
*/
|
||||
private static int[] getTopBlockMap(int channelCount)
|
||||
{
|
||||
int[] newMap = new int[channelCount * 2];
|
||||
|
||||
//Reorder the subchannel arrays to the structure needed for top-block processing
|
||||
int blockSize = channelCount / 2;
|
||||
|
||||
for(int channel = 0; channel < blockSize; channel++)
|
||||
{
|
||||
int newIndex = 2 * channel;
|
||||
int originalIndex = 2 * (blockSize - channel - 1);
|
||||
int offset = 2 * blockSize;
|
||||
|
||||
newMap[originalIndex] = newIndex;
|
||||
newMap[originalIndex + 1] = newIndex + 1;
|
||||
newMap[offset + originalIndex] = offset + newIndex;
|
||||
newMap[offset + originalIndex + 1] = offset + newIndex + 1;
|
||||
}
|
||||
|
||||
return newMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a top-block processing accumulator map that maps each interim filter and sample index product
|
||||
* to the corresponding final output index for the array that will feed the IFFT.
|
||||
*
|
||||
* @param channelCount - number of channels
|
||||
* @return output index to filter accumulator index mapping
|
||||
*/
|
||||
private static int[] getMiddleBlockMap(int channelCount)
|
||||
{
|
||||
int[] newMap = new int[channelCount * 2];
|
||||
|
||||
//Reorder the subchannel arrays to the structure needed for top-block processing
|
||||
int blockSize = channelCount / 2;
|
||||
|
||||
for(int channel = 0; channel < blockSize; channel++)
|
||||
{
|
||||
int newIndex = 2 * channel;
|
||||
int originalIndex = 2 * (blockSize - channel - 1);
|
||||
int offset = 2 * blockSize;
|
||||
|
||||
newMap[offset + originalIndex] = newIndex;
|
||||
newMap[offset + originalIndex + 1] = newIndex + 1;
|
||||
newMap[originalIndex] = offset + newIndex;
|
||||
newMap[originalIndex + 1] = offset + newIndex + 1;
|
||||
}
|
||||
|
||||
return newMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rearranges the filter coefficients to align with a contiguous sample buffer for processing efficiency.
|
||||
* @param coefficients of the polyphase filter
|
||||
* @param channelCount number of channels where each channel is an I/Q pair
|
||||
* @return filter rearranged for inline sample buffer processing
|
||||
*/
|
||||
private static float[] getAlignedFilter(float[] coefficients, int channelCount, int tapsPerChannel)
|
||||
{
|
||||
float[] filter = new float[channelCount * tapsPerChannel * 2];
|
||||
int blockSize = channelCount;
|
||||
|
||||
int coefficientPointer = 0;
|
||||
int filterPointer = 0;
|
||||
|
||||
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
|
||||
while(coefficientPointer < coefficients.length)
|
||||
{
|
||||
filter[filterPointer++] = coefficients[coefficientPointer];
|
||||
filter[filterPointer++] = coefficients[coefficientPointer++];
|
||||
}
|
||||
|
||||
//Swap each of the coefficients on block size boundaries
|
||||
for(int x = 0; x < filter.length; x += blockSize)
|
||||
{
|
||||
for(int y = 0; y < blockSize / 2; y++)
|
||||
{
|
||||
int index1 = x + y;
|
||||
int index2 = x + (blockSize - y - 1);
|
||||
float temp = filter[index2];
|
||||
filter[index2] = filter[index1];
|
||||
filter[index1] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any
|
||||
* registered channel listeners.
|
||||
*/
|
||||
private void process(ReusableChannelResultsBuffer channelResultsBuffer)
|
||||
{
|
||||
//Multiply each of the samples by the corresponding filter tap
|
||||
for(int x = 0; x < mInlineSamples.length; x++)
|
||||
{
|
||||
mInlineInterimOutput[x] = mInlineSamples[x] * mInlineFilter[x];
|
||||
}
|
||||
|
||||
Arrays.fill(mFilterAccumulator, 0.0f);
|
||||
|
||||
int tapOffset = 0;
|
||||
|
||||
//Accumulate the sample/filter product results into each of the I/Q sub-channels
|
||||
for(int tap = 0; tap < mTapsPerChannel; tap++)
|
||||
{
|
||||
tapOffset = tap * getSubChannelCount();
|
||||
|
||||
for(int channel = 0; channel < getSubChannelCount(); channel++)
|
||||
{
|
||||
mFilterAccumulator[channel] += mInlineInterimOutput[tapOffset + channel];
|
||||
}
|
||||
}
|
||||
|
||||
float[] processed = channelResultsBuffer.getEmptyBuffer(getSubChannelCount());
|
||||
|
||||
if(mTopBlockIndicator)
|
||||
{
|
||||
for(int x = 0; x < getSubChannelCount(); x++)
|
||||
{
|
||||
processed[x] = mFilterAccumulator[mTopBlockMap[x]];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(int x = 0; x < getSubChannelCount(); x++)
|
||||
{
|
||||
processed[x] = mFilterAccumulator[mMiddleBlockMap[x]];
|
||||
}
|
||||
}
|
||||
|
||||
channelResultsBuffer.addChannelResults(processed);
|
||||
|
||||
mTopBlockIndicator = !mTopBlockIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the channelizer filter structures.
|
||||
*
|
||||
* @param coefficients of the prototype filter for this channelizer
|
||||
*/
|
||||
private void init(float[] coefficients)
|
||||
{
|
||||
mFFT = new FloatFFT_1D(getChannelCount());
|
||||
int channelCount = getChannelCount();
|
||||
int bufferLength = getSubChannelCount() * mTapsPerChannel;
|
||||
mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2
|
||||
mTopBlockMap = getTopBlockMap(channelCount);
|
||||
mMiddleBlockMap = getMiddleBlockMap(channelCount);
|
||||
mInlineFilter = getAlignedFilter(coefficients, channelCount, mTapsPerChannel);
|
||||
mInlineSamples = new float[bufferLength];
|
||||
mInlineInterimOutput = new float[bufferLength];
|
||||
mFilterAccumulator = new float[getSubChannelCount()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate threaded processor to receive and enqueue filtered channel results buffers, perform IFFT on each array
|
||||
* as required to align the phase of each polyphase channel, and then dispatch the results to any registered
|
||||
* sample consumer channels.
|
||||
*/
|
||||
public class IFFTProcessor extends ContinuousBufferProcessor<ReusableChannelResultsBuffer>
|
||||
{
|
||||
public IFFTProcessor(int maximumSize, int resetThreshold)
|
||||
{
|
||||
super(maximumSize, resetThreshold);
|
||||
|
||||
//We create a listener interface to receive the buffers from the scheduled thread pool
|
||||
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each
|
||||
//channel results array contained in each results buffer and then dispatch the buffer
|
||||
//so that it can be distributed to each channel listener.
|
||||
setListener(new Listener<List<ReusableChannelResultsBuffer>>()
|
||||
{
|
||||
@Override
|
||||
public void receive(List<ReusableChannelResultsBuffer> buffers)
|
||||
{
|
||||
for(ReusableChannelResultsBuffer buffer: buffers)
|
||||
{
|
||||
for(float[] channelResults: buffer.getChannelResults())
|
||||
{
|
||||
//Rotate each of the channels to the correct phase using the IFFT
|
||||
mFFT.complexInverse(channelResults, true);
|
||||
}
|
||||
|
||||
dispatch(buffer);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setOverflowListener(new IOverflowListener()
|
||||
{
|
||||
@Override
|
||||
public void sourceOverflow(boolean overflow)
|
||||
{
|
||||
mLog.debug("IFFTProcessor overflow changed - overflow:" + overflow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any buffers from the dispatch/processing queue. Overrides the parent method so that we can set
|
||||
* the user count to 0 to allow the buffer to be reclaimed.
|
||||
*/
|
||||
protected Collection<ReusableChannelResultsBuffer> clearQueue()
|
||||
{
|
||||
Collection<ReusableChannelResultsBuffer> buffersToDispose = super.clearQueue();
|
||||
|
||||
for(ReusableChannelResultsBuffer buffer: buffersToDispose)
|
||||
{
|
||||
buffer.clearUserCount();
|
||||
}
|
||||
|
||||
return buffersToDispose;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.IOverflowListener;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.OverflowableTransferQueue;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ContinuousBufferProcessor<E> implements Listener<E>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ContinuousBufferProcessor.class);
|
||||
|
||||
private OverflowableTransferQueue<E> mQueue;
|
||||
private Listener<List<E>> mListener;
|
||||
private ScheduledFuture<?> mScheduledFuture;
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
|
||||
* for periodically distributing internally queued elements to the registered listener. This processor provides
|
||||
* a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
|
||||
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
|
||||
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
|
||||
* processing workload.
|
||||
*
|
||||
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
|
||||
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
|
||||
*
|
||||
* @param maximumSize of the internal queue (overflow happens when this is exceeded)
|
||||
* @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
|
||||
*/
|
||||
public ContinuousBufferProcessor(int maximumSize, int resetThreshold)
|
||||
{
|
||||
this(new OverflowableTransferQueue<>(maximumSize, resetThreshold));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
|
||||
* for periodically distributing internally queued elements to the registered listener. This processor provides
|
||||
* a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
|
||||
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
|
||||
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
|
||||
* processing workload.
|
||||
*
|
||||
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
|
||||
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
|
||||
*
|
||||
* @param queue implmentation of an overflowable transfer queue
|
||||
*/
|
||||
public ContinuousBufferProcessor(OverflowableTransferQueue<E> queue)
|
||||
{
|
||||
mQueue = queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be
|
||||
* invoked with a null argument to clear the previously registered overflow listener.
|
||||
*
|
||||
* @param listener to receive overflow/reset notifications.
|
||||
*/
|
||||
public void setOverflowListener(IOverflowListener listener)
|
||||
{
|
||||
mQueue.setOverflowListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or changes the listener to receive buffers from this processor.
|
||||
* @param listener to receive buffers
|
||||
*/
|
||||
public void setListener(Listener<List<E>> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this
|
||||
* processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer
|
||||
* processing.
|
||||
*
|
||||
* @param e to enqueue for distribution to a registered listener
|
||||
*/
|
||||
@Override
|
||||
public void receive(E e)
|
||||
{
|
||||
mQueue.offer(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this buffer processor and allows queuing of incoming buffers.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, 5, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
if(mScheduledFuture != null)
|
||||
{
|
||||
mScheduledFuture.cancel(true);
|
||||
mScheduledFuture = null;
|
||||
}
|
||||
|
||||
clearQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any buffers from the queue
|
||||
*/
|
||||
protected Collection<E> clearQueue()
|
||||
{
|
||||
Collection<E> clearedElements = new ArrayList<>();
|
||||
mQueue.drainTo(clearedElements);
|
||||
return clearedElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this processor is currently running
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processor to service the buffer queue and distribute the buffers to the registered listener
|
||||
*/
|
||||
class Processor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
List<E> buffers = new ArrayList<>();
|
||||
|
||||
try
|
||||
{
|
||||
mQueue.drainTo(buffers);
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(buffers);
|
||||
}
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
mLog.error("Error while dispatching buffers to listeners", throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
|
||||
public class ContinuousReusableBufferProcessor<T extends ReusableBuffer> extends ContinuousBufferProcessor<T>
|
||||
{
|
||||
/**
|
||||
* Scheduled Reusable Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing
|
||||
* task for periodically distributing internally queued elements to the registered listener. This processor
|
||||
* provides a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
|
||||
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
|
||||
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
|
||||
* processing workload.
|
||||
*
|
||||
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
|
||||
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
|
||||
*
|
||||
* @param maximumSize of the internal queue (overflow happens when this is exceeded)
|
||||
* @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
|
||||
*/
|
||||
public ContinuousReusableBufferProcessor(int maximumSize, int resetThreshold)
|
||||
{
|
||||
super(new OverflowableReusableBufferTransferQueue<T>(maximumSize, resetThreshold));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IReusableComplexBufferProvider;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventProcessor;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.tuner.TunerController;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
|
||||
* provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by
|
||||
* the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This
|
||||
* class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating
|
||||
* active DDC polyphase channel sources accordingly. This class also monitors source event requests and
|
||||
* notifications received from active DDC polyphase channel sources to adjust sample streams as required.
|
||||
*
|
||||
* Channel bandwidth and channel count are determined by the sample rate of the baseband buffer stream provider. This
|
||||
* class is currently designed to provide channels each with a minimum usable bandwidth of 12.5 kHz and oversampled by
|
||||
* 2.0 to a minimum of 25.0 kHz channel sample rate. If the baseband stream provider sample rate is not evenly
|
||||
* divisible by 12.5 kHz channels for an even number of channels, the channel bandwidth will be increased.
|
||||
*
|
||||
* Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager
|
||||
* adapts to changes in source frequency and sample rate.
|
||||
*/
|
||||
public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0;
|
||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
|
||||
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
|
||||
|
||||
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||
private IReusableComplexBufferProvider mReusableBufferProvider;
|
||||
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||
private ChannelCalculator mChannelCalculator;
|
||||
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
|
||||
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
|
||||
private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor();
|
||||
private ContinuousBufferProcessor<ReusableComplexBuffer> mBufferProcessor;
|
||||
private Map<Integer,float[]> mOutputProcessorFilters = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a polyphase channel manager instance.
|
||||
*
|
||||
* @param reusableComplexBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer
|
||||
* streams
|
||||
* @param frequency of the baseband complex buffer sample stream (ie center frequency)
|
||||
* @param sampleRate of the baseband complex buffer sample stream
|
||||
*/
|
||||
public PolyphaseChannelManager(IReusableComplexBufferProvider reusableComplexBufferProvider,
|
||||
long frequency, double sampleRate)
|
||||
{
|
||||
if(reusableComplexBufferProvider == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
|
||||
}
|
||||
|
||||
mReusableBufferProvider = reusableComplexBufferProvider;
|
||||
|
||||
int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
|
||||
|
||||
//Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer
|
||||
if(channelCount % 2 != 0)
|
||||
{
|
||||
channelCount--;
|
||||
}
|
||||
|
||||
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
|
||||
|
||||
mBufferProcessor = new ContinuousBufferProcessor(200, 50);
|
||||
mBufferProcessor.setListener(mBufferSourceEventMonitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a polyphase channel manager for the tuner controller
|
||||
*
|
||||
* @param tunerController for a tuner that provides a baseband complex buffer stream.
|
||||
*/
|
||||
public PolyphaseChannelManager(TunerController tunerController)
|
||||
{
|
||||
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Current channel sample rate which is (2 * channel bandwidth).
|
||||
*/
|
||||
public double getChannelSampleRate()
|
||||
{
|
||||
return mChannelCalculator.getChannelSampleRate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current channel bandwidth/spacing.
|
||||
*/
|
||||
public double getChannelBandwidth()
|
||||
{
|
||||
return mChannelCalculator.getChannelBandwidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be
|
||||
* sourced due to the current center frequency and/or sample rate.
|
||||
* @param tunerChannel specifying center frequency and bandwidth.
|
||||
* @return source or null.
|
||||
*/
|
||||
public TunerChannelSource getChannel(TunerChannel tunerChannel)
|
||||
{
|
||||
PolyphaseChannelSource channelSource = null;
|
||||
|
||||
try
|
||||
{
|
||||
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
|
||||
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
|
||||
|
||||
if(outputProcessor != null)
|
||||
{
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
|
||||
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
|
||||
mChannelCalculator.getChannelSampleRate(), centerFrequency);
|
||||
|
||||
mChannelSources.add(channelSource);
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" +
|
||||
mChannelCalculator.getCenterFrequency() + "] and sample rate [" +
|
||||
(mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]");
|
||||
}
|
||||
|
||||
return channelSource;
|
||||
}
|
||||
|
||||
public TunerChannelSource getTestChannel(int index)
|
||||
{
|
||||
PolyphaseChannelSource channelSource = null;
|
||||
|
||||
try
|
||||
{
|
||||
List<Integer> polyphaseIndexes = new ArrayList<>();
|
||||
polyphaseIndexes.add(index);
|
||||
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
|
||||
|
||||
|
||||
if(outputProcessor != null)
|
||||
{
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
|
||||
|
||||
TunerChannel tunerChannel = new TunerChannel(centerFrequency, 12500);
|
||||
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
|
||||
mChannelCalculator.getChannelSampleRate(), centerFrequency);
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.error("Couldn't get test channel with index:" + index, iae);
|
||||
}
|
||||
|
||||
return channelSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a processor to process the channelizer channel indexes into a composite output stream providing
|
||||
* channelized complex sample buffers to a registered source listener.
|
||||
* @param indexes to target by the output processor
|
||||
* @return output processor compatible with the number of indexes to monitor
|
||||
*/
|
||||
private IPolyphaseChannelOutputProcessor getOutputProcessor(List<Integer> indexes)
|
||||
{
|
||||
switch(indexes.size())
|
||||
{
|
||||
case 1:
|
||||
return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes);
|
||||
case 2:
|
||||
try
|
||||
{
|
||||
float[] filter = getOutputProcessorFilter(2);
|
||||
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes, filter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error designing 2 channel synthesis filter for output processor");
|
||||
}
|
||||
default:
|
||||
//TODO: create output processor for greater than 2 input channels
|
||||
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
|
||||
mLog.info(mChannelCalculator.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive
|
||||
* sample buffers when this is the first channel.
|
||||
*
|
||||
* @param channelSource to start
|
||||
*/
|
||||
private void startChannelSource(PolyphaseChannelSource channelSource)
|
||||
{
|
||||
synchronized(mBufferProcessor)
|
||||
{
|
||||
//Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method
|
||||
|
||||
checkChannelizerConfiguration();
|
||||
|
||||
mPolyphaseChannelizer.addChannel(channelSource);
|
||||
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
|
||||
|
||||
//If this is the first channel, register to start the sample buffers flowing
|
||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
|
||||
{
|
||||
mReusableBufferProvider.addBufferListener(mBufferProcessor);
|
||||
mPolyphaseChannelizer.start();
|
||||
mBufferProcessor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner
|
||||
* when this is the last channel being sourced.
|
||||
*
|
||||
* @param channelSource to stop
|
||||
*/
|
||||
private void stopChannelSource(PolyphaseChannelSource channelSource)
|
||||
{
|
||||
synchronized(mBufferProcessor)
|
||||
{
|
||||
mChannelSources.remove(channelSource);
|
||||
mPolyphaseChannelizer.removeChannel(channelSource);
|
||||
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
|
||||
|
||||
//If this is the last/only channel, deregister to stop the sample buffers
|
||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||
{
|
||||
mReusableBufferProvider.removeBufferListener(mBufferProcessor);
|
||||
mBufferProcessor.stop();
|
||||
mPolyphaseChannelizer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//Broadcast a stop sample stream notification in case this was a forced-stop so consumers are aware
|
||||
channelSource.process(SourceEvent.stopSampleStreamNotification(channelSource));
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process source events received from the source (ie tuner controller) for frequency and sample rate change
|
||||
* notifications.
|
||||
* @param sourceEvent to process
|
||||
* @throws SourceException
|
||||
*/
|
||||
@Override
|
||||
public void process(SourceEvent sourceEvent) throws SourceException
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Update channel calculator immediately so that channels can be allocated
|
||||
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
|
||||
|
||||
//Defer channelizer configuration changes to be handled on the buffer processor thread
|
||||
mBufferSourceEventMonitor.receive(sourceEvent);
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
//Update channel calculator immediately so that channels can be allocated
|
||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(sampleRate);
|
||||
mChannelCalculator.setRates(sampleRate, channelCount);
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
//no-op
|
||||
break;
|
||||
default:
|
||||
mLog.info("Unrecognized source event: " + sourceEvent.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates the channelizer to process the incoming sample rate and updates any channel processors.
|
||||
*
|
||||
* Note: this method should only be invoked on the mBufferProcessor thread or prior to starting the mBufferProcessor.
|
||||
* Sample rate source events will normally arrive via the incoming complex buffer stream from the mBufferProcessor
|
||||
* and will be handled as they arrive.
|
||||
*/
|
||||
private void checkChannelizerConfiguration()
|
||||
{
|
||||
//Channel calculator is always in sync with the tuner's current sample rate
|
||||
double tunerSampleRate = mChannelCalculator.getSampleRate();
|
||||
|
||||
//If the channelizer is not setup, or setup to the wrong sample rate, recreate it
|
||||
if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - tunerSampleRate) > 0.5)
|
||||
{
|
||||
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() > 0)
|
||||
{
|
||||
throw new IllegalStateException("Polyphase Channelizer cannot be changed to a new sample rate while " +
|
||||
"channels are currently sourced. Ensure you remove all tuner channels before changing tuner " +
|
||||
"sample rate. Current channel count:" +
|
||||
(mPolyphaseChannelizer != null ? mPolyphaseChannelizer.getRegisteredChannelCount() : "0"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(tunerSampleRate,
|
||||
POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL);
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.error("Could not create polyphase channelizer for sample rate [" + tunerSampleRate + "]", iae);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Could not create filter for polyphase channelizer for sample rate [" + tunerSampleRate + "]", fde);
|
||||
}
|
||||
|
||||
//Clear any previous channel synthesis filters so they can be recreated for the new channel sample rate
|
||||
mOutputProcessorFilters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which
|
||||
* would cause the output processors to change the polyphase channelizer results channel(s) that the processor is
|
||||
* consuming
|
||||
*
|
||||
* @param sourceEvent (optional-can be null) to broadcast to each output processor following the update
|
||||
*/
|
||||
private void updateOutputProcessors(SourceEvent sourceEvent)
|
||||
{
|
||||
for(PolyphaseChannelSource channelSource: mChannelSources)
|
||||
{
|
||||
updateOutputProcessor(channelSource);
|
||||
|
||||
//Send the non-null source event to each channel source
|
||||
if(sourceEvent != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
channelSource.process(sourceEvent);
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
mLog.error("Error while notifying polyphase channel source of a source event", se);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the polyphase channel source's output processor due to a change in the center frequency or sample
|
||||
* rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's
|
||||
* center tuned frequency changes.
|
||||
*
|
||||
* @param channelSource that requires an update to its output processor
|
||||
*/
|
||||
private void updateOutputProcessor(PolyphaseChannelSource channelSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
|
||||
//calculator will throw an IllegalArgException ... handled below
|
||||
List<Integer> indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());
|
||||
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);
|
||||
|
||||
//If the indexes size is the same then update the current processor, otherwise create a new one
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = channelSource.getPolyphaseChannelOutputProcessor();
|
||||
|
||||
if(outputProcessor != null && outputProcessor.getInputChannelCount() == indexes.size())
|
||||
{
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
|
||||
channelSource.setFrequency(centerFrequency);
|
||||
|
||||
if(indexes.size() > 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
float[] filter = getOutputProcessorFilter(indexes.size());
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setSynthesisFilter(filter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error creating an updated synthesis filter for the channel output processor");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " +
|
||||
"updated tuner center frequency and sample rate. Stopping channel source", iae);
|
||||
|
||||
stopChannelSource(channelSource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted set of currently sourced tuner channels being provided by this channel manager. The set is ordered by
|
||||
* frequency (low to high).
|
||||
*/
|
||||
public SortedSet<TunerChannel> getTunerChannels()
|
||||
{
|
||||
SortedSet<TunerChannel> tunerChannels = new TreeSet<>();
|
||||
|
||||
for(PolyphaseChannelSource channelSource: mChannelSources)
|
||||
{
|
||||
tunerChannels.add(channelSource.getTunerChannel());
|
||||
}
|
||||
|
||||
return tunerChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of currently sourced tuner channels
|
||||
*/
|
||||
public int getTunerChannelCount()
|
||||
{
|
||||
return mChannelSources.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the listener to receive source events
|
||||
*/
|
||||
public void addSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mSourceEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving source events
|
||||
*/
|
||||
public void removeSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mSourceEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates (or reuses) an output processor filter for the specified number of channels. Each
|
||||
* filter is created only once and stored in a map for reuse. This map is cleared anytime that the
|
||||
* input sample rate changes, so that the filters can be recreated with the new channel sample rate.
|
||||
* @param channels count
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed to specification (-6 dB band edge)
|
||||
*/
|
||||
private float[] getOutputProcessorFilter(int channels) throws FilterDesignException
|
||||
{
|
||||
float[] taps = mOutputProcessorFilters.get(channels);
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelBandwidth(), channels,
|
||||
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
|
||||
|
||||
mOutputProcessorFilters.put(channels, taps);
|
||||
}
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
|
||||
*/
|
||||
private class ChannelSourceEventListener implements Listener<SourceEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case REQUEST_START_SAMPLE_STREAM:
|
||||
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
|
||||
{
|
||||
startChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Request to start sample stream for unrecognized source: " +
|
||||
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
||||
}
|
||||
break;
|
||||
case REQUEST_STOP_SAMPLE_STREAM:
|
||||
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
|
||||
{
|
||||
stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Request to stop sample stream for unrecognized source: " +
|
||||
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
|
||||
}
|
||||
break;
|
||||
case REQUEST_SOURCE_DISPOSE:
|
||||
Source source = sourceEvent.getSource();
|
||||
|
||||
if(source instanceof PolyphaseChannelSource)
|
||||
{
|
||||
source.dispose();
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
//ignore
|
||||
break;
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
|
||||
//Rebroadcast so that the tuner source can process this event
|
||||
mSourceEventBroadcaster.broadcast(sourceEvent);
|
||||
break;
|
||||
default:
|
||||
mLog.error("Received unrecognized source event from polyphase channel source [" +
|
||||
sourceEvent.getEvent() + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the incoming buffer stream from the provider and transfers the buffers to the polyphase channelizer.
|
||||
*
|
||||
* This monitor incorporates a queue for receiving source events that affect configuration of the channelizer so
|
||||
* that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer
|
||||
* and/or the channel sources and output processors.
|
||||
*/
|
||||
public class BufferSourceEventMonitor implements Listener<List<ReusableComplexBuffer>>
|
||||
{
|
||||
private Queue<SourceEvent> mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/**
|
||||
* Queues the source event for deferred execution on the buffer processing thread.
|
||||
* @param event that affects configuration of the channelizer (frequency or sample rate change events)
|
||||
*/
|
||||
public void receive(SourceEvent event)
|
||||
{
|
||||
mQueuedSourceEvents.offer(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(List<ReusableComplexBuffer> reusableComplexBuffers)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Process any queued source events before processing the buffers
|
||||
SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll();
|
||||
|
||||
while(queuedSourceEvent != null)
|
||||
{
|
||||
switch(queuedSourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Don't send the tuner's frequency change event down to the channels - it would cause chaos
|
||||
updateOutputProcessors(null);
|
||||
break;
|
||||
}
|
||||
|
||||
queuedSourceEvent = mQueuedSourceEvents.poll();
|
||||
}
|
||||
|
||||
for(ReusableComplexBuffer reusableComplexBuffer: reusableComplexBuffers)
|
||||
{
|
||||
if(mPolyphaseChannelizer != null)
|
||||
{
|
||||
//User count management is handled by the channelizer
|
||||
mPolyphaseChannelizer.receive(reusableComplexBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
mLog.error("Error", throwable);
|
||||
for(ReusableComplexBuffer buffer: reusableComplexBuffers)
|
||||
{
|
||||
buffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PolyphaseChannelSource extends TunerChannelSource
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
|
||||
|
||||
private ReusableComplexBufferAssembler mReusableComplexBufferAssembler;
|
||||
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
|
||||
private double mChannelSampleRate;
|
||||
private long mIndexCenterFrequency;
|
||||
private long mChannelFrequencyCorrection;
|
||||
|
||||
/**
|
||||
* Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the
|
||||
* polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is
|
||||
* frequency translated and decimated to a single channel.
|
||||
*
|
||||
* @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate
|
||||
* @param outputProcessor - to process polyphase channelizer channel results into a channel stream
|
||||
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
|
||||
* @param channelSampleRate for the downstream sample output
|
||||
* @param centerFrequency of the incoming polyphase channel(s)
|
||||
*/
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
|
||||
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate,
|
||||
long centerFrequency)
|
||||
{
|
||||
super(producerSourceEventListener, tunerChannel);
|
||||
mPolyphaseChannelOutputProcessor = outputProcessor;
|
||||
mChannelSampleRate = channelSampleRate;
|
||||
mReusableComplexBufferAssembler = new ReusableComplexBufferAssembler(2500, mChannelSampleRate);
|
||||
setFrequency(centerFrequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive complex sample buffers from this channel source
|
||||
*/
|
||||
@Override
|
||||
public void setListener(final Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mReusableComplexBufferAssembler.setListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving complex sample buffers from this channel source
|
||||
*/
|
||||
@Override
|
||||
public void removeListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mReusableComplexBufferAssembler.setListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel output processor used by this channel source to convert polyphase channel results into a specific
|
||||
* channel complex buffer output stream.
|
||||
*/
|
||||
public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
|
||||
{
|
||||
return mPolyphaseChannelOutputProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/updates the output processor for this channel source, replacing the existing output processor.
|
||||
*
|
||||
* @param outputProcessor to use
|
||||
* @param frequency center for the channels processed by the output processor
|
||||
*/
|
||||
public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency)
|
||||
{
|
||||
//Lock on the channel output processor to block the channel results processor thread from servicing the
|
||||
//channel output processor's buffer while we change out the processors.
|
||||
synchronized(mPolyphaseChannelOutputProcessor)
|
||||
{
|
||||
IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
|
||||
|
||||
//Swap out the processor so that incoming samples can accumulate in the new channel output processor
|
||||
mPolyphaseChannelOutputProcessor = outputProcessor;
|
||||
|
||||
//Remove the buffer overflow listener and clear any previous buffer overflow state
|
||||
existingProcessor.setSourceOverflowListener(null);
|
||||
broadcastOverflowState(false);
|
||||
|
||||
//Register to receive buffer overflow notifications so we can push them up through the source chain
|
||||
mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this);
|
||||
|
||||
//Fully process the residual channel results buffer of the previous channel output processor
|
||||
if(existingProcessor != null)
|
||||
{
|
||||
existingProcessor.processChannelResults(mReusableComplexBufferAssembler);
|
||||
}
|
||||
|
||||
//Finally, setup the frequency offset for the output processor and reset the frequency correction value
|
||||
//to allow consumers to adjust and calculate a new frequency correction value, if needed.
|
||||
mIndexCenterFrequency = frequency;
|
||||
mChannelFrequencyCorrection = 0;
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method for receiving channel results output from a polyphase channelizer. The results buffer will be
|
||||
* queued for processing to extract the target channel samples, process them for frequency correction and/or
|
||||
* channel aggregation, and dispatch the results to the downstream sample listener/consumer.
|
||||
*
|
||||
* @param channelResultsBuffer containing an array of polyphase channelizer outputs.
|
||||
*/
|
||||
public void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer)
|
||||
{
|
||||
mReusableComplexBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp());
|
||||
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downstream channel sample rate
|
||||
*
|
||||
* @return sample rate in Hertz
|
||||
*/
|
||||
@Override
|
||||
public double getSampleRate()
|
||||
{
|
||||
return mChannelSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the downstream channel sample rate
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
@Override
|
||||
protected void setSampleRate(double sampleRate)
|
||||
{
|
||||
mChannelSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
if(mReusableComplexBufferAssembler != null)
|
||||
{
|
||||
mReusableComplexBufferAssembler.dispose();
|
||||
}
|
||||
|
||||
if(mPolyphaseChannelOutputProcessor != null)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.dispose();
|
||||
mPolyphaseChannelOutputProcessor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChannelFrequencyCorrection()
|
||||
{
|
||||
return mChannelFrequencyCorrection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the frequency correction value that is being applied to the channelized output stream by the
|
||||
* polyphase channel output processor.
|
||||
*
|
||||
* @param value to apply for frequency correction in hertz
|
||||
*/
|
||||
protected void setChannelFrequencyCorrection(long value)
|
||||
{
|
||||
mChannelFrequencyCorrection = value;
|
||||
updateFrequencyOffset();
|
||||
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the center frequency for this channel.frequency for the incoming sample stream channel results and resets frequency correction to zero.
|
||||
* @param frequency in hertz
|
||||
*/
|
||||
@Override
|
||||
protected void setFrequency(long frequency)
|
||||
{
|
||||
mIndexCenterFrequency = frequency;
|
||||
|
||||
//Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to
|
||||
//recalculate the frequency error correction again
|
||||
setChannelFrequencyCorrection(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processSamples()
|
||||
{
|
||||
//Lock on the output processor so that it can't be changed in the middle of processing
|
||||
synchronized(mPolyphaseChannelOutputProcessor)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.processChannelResults(mReusableComplexBufferAssembler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency offset required to mix the incoming signal to center the desired frequency
|
||||
* within the channel
|
||||
*/
|
||||
private long getFrequencyOffset()
|
||||
{
|
||||
return mIndexCenterFrequency - getTunerChannel().getFrequency() + mChannelFrequencyCorrection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the frequency offset being applied by the output processor. Calculates the offset using the center
|
||||
* frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any
|
||||
* requested frequency correction value.
|
||||
*/
|
||||
private void updateFrequencyOffset()
|
||||
{
|
||||
synchronized(mPolyphaseChannelOutputProcessor)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
|
||||
getTunerChannel().getFrequency();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.channelizer;
|
||||
|
||||
public class SampleTimestampManager
|
||||
{
|
||||
private long mReferenceTimestamp = System.currentTimeMillis();
|
||||
private double mSamplesPerMilliSecond;
|
||||
private long mSampleCount;
|
||||
|
||||
/**
|
||||
* Tracks the number of samples produced relative to a reference timestamp to derive a current reference timestamp
|
||||
* for use in generating sample buffers.
|
||||
*
|
||||
* @param sampleRate per second
|
||||
*/
|
||||
public SampleTimestampManager(double sampleRate)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal reference timestamp and resets the sample counter to zero.
|
||||
*
|
||||
* @param referenceTimestamp in milli-seconds since epoch
|
||||
*/
|
||||
public void setReferenceTimestamp(long referenceTimestamp)
|
||||
{
|
||||
mReferenceTimestamp = referenceTimestamp;
|
||||
mSampleCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sample rate that is used in calculating the reference timestamp
|
||||
*
|
||||
* @param sampleRate
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
//Convert to samples per milli-second for compatibility with reference timestamp
|
||||
mSamplesPerMilliSecond = sampleRate / 1000.0d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
|
||||
*/
|
||||
public void increment()
|
||||
{
|
||||
mSampleCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
|
||||
*
|
||||
* @param value to add to the sample count
|
||||
*/
|
||||
public void increment(int value)
|
||||
{
|
||||
mSampleCount += value;
|
||||
}
|
||||
|
||||
public long getCurrentTimestamp()
|
||||
{
|
||||
if(mSampleCount > 0)
|
||||
{
|
||||
return mReferenceTimestamp + (long)((double)mSampleCount / mSamplesPerMilliSecond);
|
||||
}
|
||||
|
||||
return mReferenceTimestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Implements a two-channel polyphase filer synthesizer. This class is intended to be used with an M2 polyphase
|
||||
* channelizer in order to recover a signal that overlaps the boundary between two channels. This synthesizer will
|
||||
* rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the output stream to
|
||||
* half of the sample rate, producing an overall M2 output sample rate.
|
||||
*
|
||||
* The synthesizer concept is modeled on the structure described by Fred Harris et al, in 'Interleaving Different
|
||||
* Bandwidth Narrowband Channels in Perfect Reconstruction Cascade Polyphase Filter Banks for Efficient Flexible
|
||||
* Variable Bandwidth Filters in Wideband Digital Transceivers'.
|
||||
*
|
||||
* The data buffers and sub-filter kernels bear some resemblance to the structures defined in the paper. The data and
|
||||
* filter structures in this class are organized to take advantage of Java's ability to leverage SIMD processor
|
||||
* intrinsics for optimal efficiency in processing. The paper specifies splitting the filter kernel into N polyphase
|
||||
* partitions and creating N data buffers. This class organizes the data buffer as a contiguous sample
|
||||
* array and creates an I/Q interleaved filter kernel. Instead of calculating the dot-product for each sub-filter,
|
||||
* we calculate the product of the full data array against the filter and then accumulate each sub-filter. This allows
|
||||
* java to use SIMD for the array product and then normal processing for the accumulation. Since this is a two-channel
|
||||
* processor and the results of each filter accumulation are added, we use a single accumulator across both filters.
|
||||
*
|
||||
*/
|
||||
public class TwoChannelSynthesizerM2
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class);
|
||||
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Two Channel Synthesizer M2");
|
||||
private float[] mSerpentineDataBuffer;
|
||||
private float[] mIQInterleavedFilter;
|
||||
private float[] mFilterVectorProduct;
|
||||
private float[] mIFFTBuffer = new float[4];
|
||||
private float mIAccumulator;
|
||||
private float mQAccumulator;
|
||||
private FloatFFT_1D mFFT = new FloatFFT_1D(2);
|
||||
private boolean mTopBlockFlag = true;
|
||||
|
||||
/**
|
||||
* Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using perfect
|
||||
* reconstruction filters (-6db at band edge). Output sample rate is the same as the input sample rate of one of
|
||||
* the channels.
|
||||
*/
|
||||
public TwoChannelSynthesizerM2(float[] filter)
|
||||
{
|
||||
init(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the synthesizer filter and data buffers for operation.
|
||||
*
|
||||
* @param filter to use for polyphase synthesis of two channels.
|
||||
*/
|
||||
private void init(float[] filter)
|
||||
{
|
||||
int tapsPerChannel = (int)Math.ceil(filter.length / 2);
|
||||
|
||||
mIQInterleavedFilter = getInterleavedFilter(filter, tapsPerChannel);
|
||||
mSerpentineDataBuffer = new float[mIQInterleavedFilter.length];
|
||||
mFilterVectorProduct = new float[mIQInterleavedFilter.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesizes a new channel from the channel 1 and channel 2 sample arrays that are arranged as
|
||||
* i0, q0, i1, q1 ... iN-1, qN-1
|
||||
*
|
||||
* @param channelBuffer1 input channel
|
||||
* @param channelBuffer2 input channel
|
||||
* @return synthesized channel results of the same length as both channel 1/2 input arrays.
|
||||
*/
|
||||
public ReusableComplexBuffer process(ReusableComplexBuffer channelBuffer1, ReusableComplexBuffer channelBuffer2)
|
||||
{
|
||||
if(channelBuffer1.getSamples().length != channelBuffer1.getSamples().length)
|
||||
{
|
||||
throw new IllegalArgumentException("Channel 1 and 2 array length must be equal");
|
||||
}
|
||||
|
||||
float[] channel1 = channelBuffer1.getSamples();
|
||||
float[] channel2 = channelBuffer2.getSamples();
|
||||
|
||||
ReusableComplexBuffer synthesizedComplexBuffer = mReusableComplexBufferQueue.getBuffer(channel1.length);
|
||||
|
||||
float[] output = synthesizedComplexBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < channel1.length; x += 2)
|
||||
{
|
||||
//Load samples from each channel into buffer for IFFT
|
||||
mIFFTBuffer[0] = channel1[x]; //i
|
||||
mIFFTBuffer[1] = channel1[x + 1]; //q
|
||||
mIFFTBuffer[2] = channel2[x]; //i
|
||||
mIFFTBuffer[3] = channel2[x + 1]; //q
|
||||
|
||||
//Perform Inverse FFT (IFFT)
|
||||
mFFT.complexInverse(mIFFTBuffer, true);
|
||||
|
||||
//Perform serpentine shift of data blocks in the data buffer - make room for 4 new samples
|
||||
System.arraycopy(mSerpentineDataBuffer, 0, mSerpentineDataBuffer, 4, mSerpentineDataBuffer.length - 4);
|
||||
|
||||
//Top Block - load samples into data buffer in normal order
|
||||
if(mTopBlockFlag)
|
||||
{
|
||||
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 0, mIFFTBuffer.length);
|
||||
}
|
||||
//Bottom Block - swap samples via data loading to account for phase shift
|
||||
else
|
||||
{
|
||||
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 2, 2);
|
||||
System.arraycopy(mIFFTBuffer, 2, mSerpentineDataBuffer, 0, 2);
|
||||
}
|
||||
|
||||
//Note: in order to use Java's ability to leverage SIMD intrinsics, we perform filtering in two steps
|
||||
//(multiply then accumulate) since our filter and data are structured with I and Q vectors interleaved.
|
||||
//This approach allows the Hotspot compiler to more easily recognize the scalor operations.
|
||||
|
||||
//Multiply data samples by the I/Q interleaved filter to form the vector product
|
||||
for(int y = 0; y < mSerpentineDataBuffer.length; y++)
|
||||
{
|
||||
mFilterVectorProduct[y] = mSerpentineDataBuffer[y] * mIQInterleavedFilter[y];
|
||||
}
|
||||
|
||||
//Accumulate output I/Q samples from vector product
|
||||
mIAccumulator = 0.0f;
|
||||
mQAccumulator = 0.0f;
|
||||
|
||||
for(int y = 0; y < mFilterVectorProduct.length; y += 2)
|
||||
{
|
||||
mIAccumulator += mFilterVectorProduct[y];
|
||||
mQAccumulator += mFilterVectorProduct[y + 1];
|
||||
}
|
||||
|
||||
output[x] = mIAccumulator;
|
||||
output[x + 1] = mQAccumulator;
|
||||
|
||||
mTopBlockFlag = !mTopBlockFlag;
|
||||
}
|
||||
|
||||
channelBuffer1.decrementUserCount();
|
||||
channelBuffer2.decrementUserCount();
|
||||
|
||||
return synthesizedComplexBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interleaved I/Q filter where each coefficient from the filter argument is duplicated and the returned
|
||||
* filter is twice the length of the original filter.
|
||||
*
|
||||
* Note: the returned filter array is sized to: 2 * channel count * taps per channel, which may be slightly more
|
||||
* than twice the length of the original filter. Any Added filter array elements will contain zero values.
|
||||
*
|
||||
* @param coefficients to create an interleaved filter
|
||||
* @param tapsPerChannel count
|
||||
* @return
|
||||
*/
|
||||
private static float[] getInterleavedFilter(float[] coefficients, int tapsPerChannel)
|
||||
{
|
||||
int channelCount = 2;
|
||||
|
||||
float[] filter = new float[channelCount * tapsPerChannel * 2];
|
||||
|
||||
int coefficientPointer = 0;
|
||||
int filterPointer = 0;
|
||||
|
||||
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
|
||||
while(coefficientPointer < coefficients.length)
|
||||
{
|
||||
filter[filterPointer++] = coefficients[coefficientPointer];
|
||||
filter[filterPointer++] = coefficients[coefficientPointer++];
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.dsp.mixer.IOscillator;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
import io.github.dsheirer.sample.IOverflowListener;
|
||||
import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class);
|
||||
|
||||
private OverflowableReusableBufferTransferQueue<ReusableChannelResultsBuffer> mChannelResultsQueue;
|
||||
private List<ReusableChannelResultsBuffer> mChannelResultsToProcess = new ArrayList<>();
|
||||
private int mMaxResultsToProcess;
|
||||
|
||||
private int mInputChannelCount;
|
||||
private IOscillator mFrequencyCorrectionMixer;
|
||||
private boolean mFrequencyCorrectionEnabled;
|
||||
|
||||
/**
|
||||
* Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
|
||||
* oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer.
|
||||
*
|
||||
* @param inputChannelCount is the number of input channels for this output processor
|
||||
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
|
||||
* channel sample rate for frequency translation/correction.
|
||||
*/
|
||||
public ChannelOutputProcessor(int inputChannelCount, double sampleRate)
|
||||
{
|
||||
mInputChannelCount = inputChannelCount;
|
||||
|
||||
//TODO: swap this out and use the LowPhaseNoiseOscillator
|
||||
mFrequencyCorrectionMixer = new Oscillator(0, sampleRate);
|
||||
mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate
|
||||
|
||||
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPolyphaseChannelIndexCount()
|
||||
{
|
||||
return mInputChannelCount;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
if(mChannelResultsQueue != null)
|
||||
{
|
||||
mChannelResultsQueue.dispose();
|
||||
mChannelResultsQueue = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
|
||||
* @param frequencyOffset in hertz
|
||||
*/
|
||||
@Override
|
||||
public void setFrequencyOffset(long frequencyOffset)
|
||||
{
|
||||
mFrequencyCorrectionMixer.setFrequency(frequencyOffset);
|
||||
mFrequencyCorrectionEnabled = (frequencyOffset != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Oscillator/mixer to use for frequency correction against incoming sample stream
|
||||
*/
|
||||
protected IOscillator getFrequencyCorrectionMixer()
|
||||
{
|
||||
return mFrequencyCorrectionMixer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveChannelResults(ReusableChannelResultsBuffer channelResults)
|
||||
{
|
||||
mChannelResultsQueue.offer(channelResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all enqueued polyphase channelizer results until the internal queue is empty
|
||||
* @param reusableComplexBufferAssembler to receive the processed channel results
|
||||
*/
|
||||
@Override
|
||||
public void processChannelResults(ReusableComplexBufferAssembler reusableComplexBufferAssembler)
|
||||
{
|
||||
try
|
||||
{
|
||||
int toProcess = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess);
|
||||
|
||||
if(toProcess > 0)
|
||||
{
|
||||
process(mChannelResultsToProcess, reusableComplexBufferAssembler);
|
||||
}
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
mLog.error("Error while processing polyphase channel samples", throwable);
|
||||
}
|
||||
|
||||
mChannelResultsToProcess.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-class implementation to process one polyphase channelizer result array.
|
||||
* @param channelResults to process
|
||||
* @param reusableComplexBufferAssembler to receive the processed channelizer results
|
||||
*/
|
||||
public abstract void process(List<ReusableChannelResultsBuffer> channelResults,
|
||||
ReusableComplexBufferAssembler reusableComplexBufferAssembler);
|
||||
|
||||
|
||||
/**
|
||||
* Sets the overflow listener to monitor the internal channelizer channel results queue overflow state
|
||||
*/
|
||||
public void setSourceOverflowListener(Source source)
|
||||
{
|
||||
mChannelResultsQueue.setSourceOverflowListener(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state
|
||||
*/
|
||||
public void removeOverflowListener(IOverflowListener listener)
|
||||
{
|
||||
mChannelResultsQueue.setOverflowListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInputChannelCount()
|
||||
{
|
||||
return mInputChannelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this channel has a frequency correction offset
|
||||
*/
|
||||
public boolean hasFrequencyCorrection()
|
||||
{
|
||||
return mFrequencyCorrectionEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
|
||||
import io.github.dsheirer.source.Source;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IPolyphaseChannelOutputProcessor
|
||||
{
|
||||
/**
|
||||
* Receive and enqueue output results from the polyphase analysis channelizer
|
||||
* @param channelResultsBuffer to enqueue
|
||||
*/
|
||||
void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer);
|
||||
|
||||
/**
|
||||
* Process the channel output channel results queue and deliver the output to the listener
|
||||
*
|
||||
* @param reusableComplexBufferAssembler to receive the processed channel results
|
||||
*/
|
||||
void processChannelResults(ReusableComplexBufferAssembler reusableComplexBufferAssembler);
|
||||
|
||||
/**
|
||||
* Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset
|
||||
* frequency to produce an output where the desired signal is centered in the passband.
|
||||
*
|
||||
* @param frequency in hertz
|
||||
*/
|
||||
void setFrequencyOffset(long frequency);
|
||||
|
||||
/**
|
||||
* Indicates the number of input channels processed by this output processor
|
||||
* @return
|
||||
*/
|
||||
int getInputChannelCount();
|
||||
|
||||
/**
|
||||
* Updates the input polyphase channel index(es) used by this output processor
|
||||
* @param indexes
|
||||
*/
|
||||
void setPolyphaseChannelIndices(List<Integer> indexes);
|
||||
|
||||
/**
|
||||
* List of current polyphase channel indices for the output processor
|
||||
*/
|
||||
int getPolyphaseChannelIndexCount();
|
||||
|
||||
/**
|
||||
* Updates the synthesis filter taps for this output processor
|
||||
* @param filter
|
||||
*/
|
||||
void setSynthesisFilter(float[] filter);
|
||||
|
||||
/**
|
||||
* Sets the listener to receive buffer overflow notifications
|
||||
* @param source to receive overflow notifications
|
||||
*/
|
||||
void setSourceOverflowListener(Source source);
|
||||
|
||||
|
||||
void dispose();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class);
|
||||
|
||||
private int mChannelOffset;
|
||||
|
||||
/**
|
||||
* Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample
|
||||
* from each polyphase channelizer output results array.
|
||||
*
|
||||
* @param sampleRate of the output sample stream.
|
||||
* @param channelIndexes containing a single channel index.
|
||||
*/
|
||||
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes)
|
||||
{
|
||||
super(1, sampleRate);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSynthesisFilter(float[] filter)
|
||||
{
|
||||
throw new IllegalArgumentException("The one channel output processor does not support filter updates");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this processor to extract a single, specified channel index.
|
||||
* @param indexes containing a single channel index value.
|
||||
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
|
||||
*/
|
||||
public void setPolyphaseChannelIndices(List<Integer> indexes)
|
||||
{
|
||||
if(indexes.size() != 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Single channel output processor requires a single index to " +
|
||||
"process - provided indexes " + indexes.toString());
|
||||
}
|
||||
|
||||
//Set the channelized output results offset to twice the channel index to account for each channel having
|
||||
//an I/Q pair
|
||||
mChannelOffset = indexes.get(0) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the channel from the channel results array, apply frequency translation, and deliver the
|
||||
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
|
||||
*
|
||||
* @param channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
* @param reusableComplexBufferAssembler to receive the extracted, frequency-translated channel results
|
||||
*/
|
||||
@Override
|
||||
public void process(List<ReusableChannelResultsBuffer> channelResults,
|
||||
ReusableComplexBufferAssembler reusableComplexBufferAssembler)
|
||||
{
|
||||
for(ReusableChannelResultsBuffer channelResultsBuffer: channelResults)
|
||||
{
|
||||
try
|
||||
{
|
||||
ReusableComplexBuffer channelBuffer = channelResultsBuffer.getChannel(mChannelOffset);
|
||||
|
||||
if(hasFrequencyCorrection())
|
||||
{
|
||||
getFrequencyCorrectionMixer().mixComplex(channelBuffer.getSamples());
|
||||
}
|
||||
|
||||
reusableComplexBufferAssembler.receive(channelBuffer);
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.error("Error extracting channel samples from polyphase channel results buffer");
|
||||
}
|
||||
|
||||
channelResultsBuffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.mixer.FS4DownConverter;
|
||||
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
|
||||
|
||||
private static Map<Integer,float[]> sLowPassFilterKernelMap = new HashMap<>();
|
||||
private TwoChannelSynthesizerM2 mSynthesizer;
|
||||
private FS4DownConverter mFS4DownConverter = new FS4DownConverter();
|
||||
private ComplexFIRFilter2 mLowPassFilter;
|
||||
|
||||
private int mChannelOffset1;
|
||||
private int mChannelOffset2;
|
||||
|
||||
/**
|
||||
* Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply
|
||||
* frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample.
|
||||
*
|
||||
* @param sampleRate of the output sample stream.
|
||||
* @param channelIndexes containing two channel indices.
|
||||
*/
|
||||
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter)
|
||||
{
|
||||
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
|
||||
//after synthesizing both input channels
|
||||
super(2, sampleRate);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
setSynthesisFilter(filter);
|
||||
|
||||
float[] lowPassFilter = sLowPassFilterKernelMap.get((int)sampleRate);
|
||||
|
||||
if(lowPassFilter == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
lowPassFilter = getLowPassFilter(sampleRate);
|
||||
sLowPassFilterKernelMap.put((int)sampleRate, lowPassFilter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Couldn't design a low-pass filter for 2-channel synthesizer", fde);
|
||||
throw new IllegalStateException("Cannot design low-pass filter for 2 channel synthesizer", fde);
|
||||
}
|
||||
}
|
||||
|
||||
mLowPassFilter = new ComplexFIRFilter2(lowPassFilter, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
|
||||
* @param frequencyOffset in hertz
|
||||
*/
|
||||
@Override
|
||||
public void setFrequencyOffset(long frequencyOffset)
|
||||
{
|
||||
super.setFrequencyOffset(frequencyOffset);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setSynthesisFilter(float[] filter)
|
||||
{
|
||||
mSynthesizer = new TwoChannelSynthesizerM2(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this processor to extract the two specified channel indexes.
|
||||
*
|
||||
* @param indexes containing a single channel index value.
|
||||
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
|
||||
*/
|
||||
public void setPolyphaseChannelIndices(List<Integer> indexes)
|
||||
{
|
||||
if(indexes.size() != 2)
|
||||
{
|
||||
throw new IllegalArgumentException("Double channel output processor requires two indexes to " +
|
||||
"process - provided indexes " + indexes.toString());
|
||||
}
|
||||
|
||||
//Set the channelized output results offsets to twice the channel index to account for each channel having
|
||||
//an I/Q pair
|
||||
mChannelOffset1 = indexes.get(0) * 2;
|
||||
mChannelOffset2 = indexes.get(1) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the channel from the channel results array, apply frequency translation, and deliver the
|
||||
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
|
||||
*
|
||||
* @param channelResultsBuffers to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
* @param reusableComplexBufferAssembler to receive the extracted, frequency-translated channel results
|
||||
*/
|
||||
@Override
|
||||
public void process(List<ReusableChannelResultsBuffer> channelResultsBuffers,
|
||||
ReusableComplexBufferAssembler reusableComplexBufferAssembler)
|
||||
{
|
||||
for(ReusableChannelResultsBuffer buffer: channelResultsBuffers)
|
||||
{
|
||||
ReusableComplexBuffer channel1 = buffer.getChannel(mChannelOffset1);
|
||||
ReusableComplexBuffer channel2 = buffer.getChannel(mChannelOffset2);
|
||||
|
||||
//Join the two channels using the synthesizer
|
||||
ReusableComplexBuffer synthesized = mSynthesizer.process(channel1, channel2);
|
||||
|
||||
//The synthesized channels are centered at +FS/4 ... downconvert to center the spectrum
|
||||
mFS4DownConverter.mixComplex(synthesized.getSamples());
|
||||
|
||||
//Apply offset and frequency correction to center the signal of interest within the synthesized channel
|
||||
getFrequencyCorrectionMixer().mixComplex(synthesized.getSamples());
|
||||
|
||||
ReusableComplexBuffer lowPassFiltered = mLowPassFilter.filter(synthesized);
|
||||
|
||||
reusableComplexBufferAssembler.receive(lowPassFiltered);
|
||||
|
||||
buffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a low-pass filter kernel for the specified sample rate.
|
||||
* @param sampleRate for the channel
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed
|
||||
*/
|
||||
private static float[] getLowPassFilter(double sampleRate) throws FilterDesignException
|
||||
{
|
||||
double cutoff = sampleRate / 4.0;
|
||||
int passBandCutoff = (int)(cutoff * 0.92);
|
||||
int stopBandStart = (int)(cutoff * 1.08);
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(sampleRate)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff((int)passBandCutoff)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart((int)stopBandStart)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.005) //Approximately 94 dB attenuation
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
return designer.getImpulseResponse();
|
||||
}
|
||||
|
||||
throw new FilterDesignException("Couldn't design low-pass filter for sample rate: " + sampleRate +
|
||||
" Pass End:" + passBandCutoff + " Stop Begin:" + stopBandStart);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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 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.
|
||||
* 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/>
|
||||
* 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.filter.cic;
|
||||
@@ -21,19 +18,19 @@ package io.github.dsheirer.dsp.filter.cic;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.Window;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
|
||||
import io.github.dsheirer.dsp.filter.halfband.complex.HalfBandFilter_CB_CB;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.halfband.complex.ComplexHalfBandFilter;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler;
|
||||
import io.github.dsheirer.sample.decimator.ComplexDecimator;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
public class ComplexPrimeCICDecimate implements Listener<ReusableComplexBuffer>
|
||||
{
|
||||
public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
|
||||
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
|
||||
@@ -87,7 +84,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Wire the current stage to the previous stage */
|
||||
/* Wire the current stage to the previous stage */
|
||||
mDecimatingStages.get(x - 1).setListener(stage);
|
||||
}
|
||||
}
|
||||
@@ -116,7 +113,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
/**
|
||||
* Adds a listener to receive the output of this CIC decimation filter
|
||||
*/
|
||||
public void setListener(Listener<ComplexBuffer> listener)
|
||||
public void setListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mOutput.setListener(listener);
|
||||
}
|
||||
@@ -166,7 +163,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
* Primary input method for receiving sample arrays composed as I,Q,I,Q, etc.
|
||||
*/
|
||||
@Override
|
||||
public void receive(ComplexBuffer buffer)
|
||||
public void receive(ReusableComplexBuffer buffer)
|
||||
{
|
||||
if(mFirstDecimatingStage != null)
|
||||
{
|
||||
@@ -177,6 +174,8 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
mFirstDecimatingStage.receive(samples[x], samples[x + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
buffer.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,11 +288,11 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
|
||||
public void receive(float i, float q)
|
||||
{
|
||||
/* Subtract the oldest sample and add back in the newest */
|
||||
/* Subtract the oldest sample and add back in the newest */
|
||||
mISum = mISum - mISamples[mSamplePointer] + i;
|
||||
mQSum = mQSum - mQSamples[mSamplePointer] + q;
|
||||
|
||||
/* Overwrite the oldest sample with the newest */
|
||||
/* Overwrite the oldest sample with the newest */
|
||||
mISamples[mSamplePointer] = i;
|
||||
mQSamples[mSamplePointer] = q;
|
||||
|
||||
@@ -352,27 +351,37 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
public class Output implements ComplexSampleListener
|
||||
{
|
||||
/* Decimated output buffers will contain 1024 samples */
|
||||
private ComplexToComplexBufferAssembler mAssembler =
|
||||
new ComplexToComplexBufferAssembler(2048);
|
||||
private ReusableComplexBufferAssembler mAssembler = new ReusableComplexBufferAssembler(2048, 48000.0);
|
||||
|
||||
private ComplexFIRFilter_CB_CB mCleanupFilter;
|
||||
private HalfBandFilter_CB_CB mHalfBandFilter = new HalfBandFilter_CB_CB(
|
||||
private ComplexFIRFilter2 mCleanupFilter;
|
||||
private ComplexHalfBandFilter mHalfBandFilter = new ComplexHalfBandFilter(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false);
|
||||
private Listener<ReusableComplexBuffer> mReusableComplexBufferListener;
|
||||
|
||||
public Output(int outputSampleRate, int passFrequency, int attenuation,
|
||||
Window.WindowType windowType)
|
||||
{
|
||||
mCleanupFilter = new ComplexFIRFilter_CB_CB(FilterFactory
|
||||
.getCICCleanupFilter(outputSampleRate,
|
||||
passFrequency,
|
||||
attenuation,
|
||||
windowType), 0.4f);
|
||||
mCleanupFilter = new ComplexFIRFilter2(FilterFactory
|
||||
.getCICCleanupFilter(outputSampleRate, passFrequency, attenuation, windowType), 0.4f);
|
||||
|
||||
//Bypassing the CIC cleanup filter for now
|
||||
mAssembler.setListener(mHalfBandFilter);
|
||||
|
||||
// mAssembler.setListener( mCleanupFilter );
|
||||
// mCleanupFilter.setListener( mHalfBandFilter );
|
||||
mAssembler.setListener(new Listener<ReusableComplexBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
{
|
||||
if(mReusableComplexBufferListener != null)
|
||||
{
|
||||
ReusableComplexBuffer cleanupFiltered = mCleanupFilter.filter(reusableComplexBuffer);
|
||||
ReusableComplexBuffer halfBandFiltered = mHalfBandFilter.filter(cleanupFiltered);
|
||||
mReusableComplexBufferListener.receive(halfBandFiltered);
|
||||
}
|
||||
else
|
||||
{
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -390,23 +399,17 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
mAssembler.receive(inphase, quadrature);
|
||||
mAssembler.receive(new float[]{inphase, quadrature});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive output samples
|
||||
*/
|
||||
public void setListener(Listener<ComplexBuffer> listener)
|
||||
public void setListener(Listener<ReusableComplexBuffer> listener)
|
||||
{
|
||||
mHalfBandFilter.setListener(listener);
|
||||
mReusableComplexBufferListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving output samples
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mHalfBandFilter.removeListener();
|
||||
mReusableComplexBufferListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
@@ -20,13 +20,13 @@ package io.github.dsheirer.dsp.filter.cic;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferAssembler;
|
||||
import io.github.dsheirer.sample.decimator.RealDecimator;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -34,381 +34,389 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RealPrimeCICDecimate
|
||||
public class RealPrimeCICDecimate
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( RealPrimeCICDecimate.class );
|
||||
|
||||
public static int[] PRIMES = { 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,
|
||||
59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,
|
||||
157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,
|
||||
257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,
|
||||
367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,
|
||||
467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,
|
||||
599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691 };
|
||||
|
||||
private ArrayList<DecimatingStage> mDecimatingStages =
|
||||
new ArrayList<DecimatingStage>();
|
||||
|
||||
private DecimatingStage mFirstDecimatingStage;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealPrimeCICDecimate.class);
|
||||
|
||||
private Output mOutput;
|
||||
|
||||
private int mOutputBufferSize = 2048;
|
||||
public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
|
||||
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
|
||||
157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251,
|
||||
257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359,
|
||||
367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
|
||||
467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
|
||||
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691};
|
||||
|
||||
/**
|
||||
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
|
||||
* decimated float sample output.
|
||||
*
|
||||
* Implements the CIC filter described in Understanding Digital Signal
|
||||
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
|
||||
* decimating stages each with a prime factor decimation rate. Multiple
|
||||
* stages are cascaded to achieve the overall decimation rate.
|
||||
*
|
||||
* This filter supports a maximum decimation rate of 700. This filter can
|
||||
* be adapted to higher decimation rates by adding additional prime factors
|
||||
* to the PRIMES array.
|
||||
*
|
||||
* @param decimation - overall decimation rate
|
||||
* @param order - filter order
|
||||
*/
|
||||
public RealPrimeCICDecimate( int decimation, int order, int passFrequency,
|
||||
int attenuation, WindowType windowType )
|
||||
{
|
||||
this( decimation, order, passFrequency, attenuation, windowType, 2048 );
|
||||
}
|
||||
|
||||
public RealPrimeCICDecimate( int decimation, int order, int passFrequency,
|
||||
int attenuation, WindowType windowType, int outputBufferSize )
|
||||
{
|
||||
Validate.isTrue(decimation <= 700);
|
||||
private List<DecimatingStage> mDecimatingStages = new ArrayList<DecimatingStage>();
|
||||
private DecimatingStage mFirstDecimatingStage;
|
||||
private Output mOutput;
|
||||
|
||||
mOutputBufferSize = outputBufferSize;
|
||||
private int mOutputBufferSize = 2048;
|
||||
|
||||
List<Integer> stageSizes = getPrimeFactors( decimation );
|
||||
|
||||
for( int x = 0; x < stageSizes.size(); x++ )
|
||||
{
|
||||
DecimatingStage stage = new DecimatingStage( stageSizes.get( x ), order );
|
||||
|
||||
mDecimatingStages.add( stage );
|
||||
|
||||
if( x == 0 )
|
||||
{
|
||||
/* Reference to first stage -- will receive all samples */
|
||||
mFirstDecimatingStage = stage;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Wire the current stage to the previous stage */
|
||||
mDecimatingStages.get( x - 1 ).setListener( stage );
|
||||
}
|
||||
}
|
||||
|
||||
mOutput = new Output( 48000, passFrequency, attenuation, windowType,
|
||||
outputBufferSize );
|
||||
|
||||
mDecimatingStages.get( mDecimatingStages.size() - 1 )
|
||||
.setListener( mOutput );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
for( DecimatingStage stage: mDecimatingStages )
|
||||
{
|
||||
stage.dispose();
|
||||
}
|
||||
|
||||
mDecimatingStages.clear();
|
||||
mDecimatingStages = null;
|
||||
mFirstDecimatingStage = null;
|
||||
|
||||
mOutput.dispose();
|
||||
mOutput = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive the output of this CIC decimation filter
|
||||
*/
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mOutput.setListener( listener );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes listener from output of this CIC decimation filter
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mOutput.removeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the prime factors of the decimation rate up to a maximum
|
||||
* decimation rate of 700. If you wish to have decimation rates higher
|
||||
* than 700, then add additional prime factors to the PRIMES array.
|
||||
*
|
||||
* @param decimation - integral decimation rate
|
||||
* @return - ordered list (smallest to largest) of prime factors
|
||||
*/
|
||||
public static List<Integer> getPrimeFactors( int decimation )
|
||||
{
|
||||
ArrayList<Integer> stages = new ArrayList<Integer>();
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
while( decimation > 0 && pointer < PRIMES.length )
|
||||
{
|
||||
int prime = PRIMES[ pointer ];
|
||||
|
||||
if( decimation % prime == 0 )
|
||||
{
|
||||
stages.add( prime );
|
||||
|
||||
decimation /= prime;
|
||||
}
|
||||
else
|
||||
{
|
||||
pointer++;
|
||||
}
|
||||
}
|
||||
|
||||
return stages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary input method for receiving real buffers.
|
||||
*/
|
||||
public void receive( RealBuffer buffer )
|
||||
/**
|
||||
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
|
||||
* decimated float sample output.
|
||||
*
|
||||
* Implements the CIC filter described in Understanding Digital Signal
|
||||
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
|
||||
* decimating stages each with a prime factor decimation rate. Multiple
|
||||
* stages are cascaded to achieve the overall decimation rate.
|
||||
*
|
||||
* This filter supports a maximum decimation rate of 700. This filter can
|
||||
* be adapted to higher decimation rates by adding additional prime factors
|
||||
* to the PRIMES array.
|
||||
*
|
||||
* @param decimation - overall decimation rate
|
||||
* @param order - filter order
|
||||
*/
|
||||
public RealPrimeCICDecimate(int decimation, int order, int passFrequency, int attenuation, WindowType windowType)
|
||||
{
|
||||
if( mFirstDecimatingStage != null )
|
||||
{
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
mFirstDecimatingStage.receive( sample );
|
||||
}
|
||||
}
|
||||
this(decimation, order, passFrequency, attenuation, windowType, 2048);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimating stage combines multiple CIC stages with a decimator. The
|
||||
* number of stages is indicated by the order value and the size indicates
|
||||
* the decimation rate of this stage.
|
||||
*/
|
||||
public class DecimatingStage implements RealSampleListener
|
||||
{
|
||||
private ArrayList<Stage> mStages = new ArrayList<Stage>();
|
||||
private Stage mFirstStage;
|
||||
private RealDecimator mDecimator;
|
||||
|
||||
public DecimatingStage( int size, int order )
|
||||
{
|
||||
for( int x = 0; x < order; x++ )
|
||||
{
|
||||
Stage stage;
|
||||
|
||||
if( size == 2 )
|
||||
{
|
||||
stage = new TwoStage();
|
||||
}
|
||||
else
|
||||
{
|
||||
stage = new Stage( size );
|
||||
}
|
||||
|
||||
mStages.add( stage );
|
||||
|
||||
if( x == 0 )
|
||||
{
|
||||
mFirstStage = stage;
|
||||
}
|
||||
else
|
||||
{
|
||||
mStages.get( x - 1 ).setListener( stage );
|
||||
}
|
||||
}
|
||||
|
||||
mDecimator = new RealDecimator( size );
|
||||
|
||||
mStages.get( mStages.size() - 1 ).setListener( mDecimator );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
for( Stage stage: mStages )
|
||||
{
|
||||
stage.dispose();
|
||||
}
|
||||
|
||||
mStages.clear();
|
||||
mDecimator.dispose();
|
||||
mDecimator = null;
|
||||
mFirstStage = null;
|
||||
mStages = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
public RealPrimeCICDecimate(int decimation, int order, int passFrequency, int attenuation, WindowType windowType, int outputBufferSize)
|
||||
{
|
||||
Validate.isTrue(decimation <= 700);
|
||||
|
||||
mOutputBufferSize = outputBufferSize;
|
||||
|
||||
List<Integer> stageSizes = getPrimeFactors(decimation);
|
||||
|
||||
for(int x = 0; x < stageSizes.size(); x++)
|
||||
{
|
||||
mFirstStage.receive( sample );
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mDecimator.setListener( listener );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single non-decimating CIC stage component. Uses a circular buffer and a
|
||||
* running average internally to implement the stage so that stage size has
|
||||
* essentially no impact on the computational requirements of the stage
|
||||
*/
|
||||
public class Stage implements RealSampleListener
|
||||
{
|
||||
protected RealSampleListener mListener;
|
||||
|
||||
private float[] mSamples;
|
||||
|
||||
protected float mSum;
|
||||
|
||||
private int mSamplePointer = 0;
|
||||
private int mSize;
|
||||
|
||||
protected float mGain;
|
||||
|
||||
public Stage()
|
||||
{
|
||||
}
|
||||
DecimatingStage stage = new DecimatingStage(stageSizes.get(x), order);
|
||||
|
||||
public Stage( int size )
|
||||
{
|
||||
mSize = size - 1;
|
||||
|
||||
mSamples = new float[ mSize ];
|
||||
|
||||
mGain = 1.0f / (float)size;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void receive( float sample )
|
||||
{
|
||||
/* Subtract the oldest sample and add back in the newest */
|
||||
mSum = mSum - mSamples[ mSamplePointer ] + sample;
|
||||
mDecimatingStages.add(stage);
|
||||
|
||||
/* Overwrite the oldest sample with the newest */
|
||||
mSamples[ mSamplePointer ] = sample;
|
||||
|
||||
mSamplePointer++;
|
||||
|
||||
if( mSamplePointer >= mSize )
|
||||
{
|
||||
mSamplePointer = 0;
|
||||
}
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( mSum * mGain );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Size 2 stage that removes the unnecessary circular buffer management for
|
||||
* a two-stage.
|
||||
*/
|
||||
public class TwoStage extends Stage
|
||||
{
|
||||
public TwoStage()
|
||||
{
|
||||
super();
|
||||
|
||||
mGain = 0.5f;
|
||||
}
|
||||
|
||||
public void receive( float sample )
|
||||
{
|
||||
float sum = mSum + sample;
|
||||
|
||||
mSum = sample;
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( sum * mGain );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output adapter - applies gain correction, applies cleanup filter, casts
|
||||
* double-valued samples into float-valued complex sample and sends the
|
||||
* result to the registered listener.
|
||||
*/
|
||||
public class Output implements RealSampleListener
|
||||
{
|
||||
/* Decimated output buffers will contain 1024 samples */
|
||||
private RealToRealBufferAssembler mAssembler;
|
||||
|
||||
private RealFIRFilter_RB_RB mCleanupFilter;
|
||||
private HalfBandFilter_RB_RB mHalfBandFilter = new HalfBandFilter_RB_RB(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false );
|
||||
|
||||
public Output( int outputSampleRate, int passFrequency, int attenuation,
|
||||
WindowType windowType, int outputBufferSize )
|
||||
{
|
||||
mCleanupFilter = new RealFIRFilter_RB_RB( FilterFactory
|
||||
.getCICCleanupFilter( outputSampleRate,
|
||||
passFrequency,
|
||||
attenuation,
|
||||
windowType ), 0.4f );
|
||||
|
||||
mAssembler = new RealToRealBufferAssembler( outputBufferSize );
|
||||
mAssembler.setListener( mCleanupFilter );
|
||||
mCleanupFilter.setListener( mHalfBandFilter );
|
||||
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mAssembler.dispose();
|
||||
mCleanupFilter.dispose();
|
||||
mHalfBandFilter.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver method for the output adapter to receive a filtered,
|
||||
* decimated sample, apply gain correction, apply cleanup filtering
|
||||
* and output the sample values as a complex sample.
|
||||
*/
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
mAssembler.receive( sample * 32.0f );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive output samples
|
||||
*/
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mHalfBandFilter.setListener( listener );
|
||||
if(x == 0)
|
||||
{
|
||||
/* Reference to first stage -- will receive all samples */
|
||||
mFirstDecimatingStage = stage;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Wire the current stage to the previous stage */
|
||||
mDecimatingStages.get(x - 1).setListener(stage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving output samples
|
||||
*/
|
||||
mOutput = new Output(48000, passFrequency, attenuation, windowType,
|
||||
outputBufferSize);
|
||||
|
||||
mDecimatingStages.get(mDecimatingStages.size() - 1)
|
||||
.setListener(mOutput);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
for(DecimatingStage stage : mDecimatingStages)
|
||||
{
|
||||
stage.dispose();
|
||||
}
|
||||
|
||||
mDecimatingStages.clear();
|
||||
mDecimatingStages = null;
|
||||
mFirstDecimatingStage = null;
|
||||
|
||||
mOutput.dispose();
|
||||
mOutput = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive the output of this CIC decimation filter
|
||||
*/
|
||||
public void setListener(Listener<ReusableBuffer> listener)
|
||||
{
|
||||
mOutput.setListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes listener from output of this CIC decimation filter
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mOutput.removeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the prime factors of the decimation rate up to a maximum
|
||||
* decimation rate of 700. If you wish to have decimation rates higher
|
||||
* than 700, then add additional prime factors to the PRIMES array.
|
||||
*
|
||||
* @param decimation - integral decimation rate
|
||||
* @return - ordered list (smallest to largest) of prime factors
|
||||
*/
|
||||
public static List<Integer> getPrimeFactors(int decimation)
|
||||
{
|
||||
ArrayList<Integer> stages = new ArrayList<Integer>();
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
while(decimation > 0 && pointer < PRIMES.length)
|
||||
{
|
||||
int prime = PRIMES[pointer];
|
||||
|
||||
if(decimation % prime == 0)
|
||||
{
|
||||
stages.add(prime);
|
||||
|
||||
decimation /= prime;
|
||||
}
|
||||
else
|
||||
{
|
||||
pointer++;
|
||||
}
|
||||
}
|
||||
|
||||
return stages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary input method for receiving real buffers.
|
||||
*/
|
||||
public void receive(ReusableBuffer buffer)
|
||||
{
|
||||
if(mFirstDecimatingStage != null)
|
||||
{
|
||||
for(float sample : buffer.getSamples())
|
||||
{
|
||||
mFirstDecimatingStage.receive(sample);
|
||||
}
|
||||
}
|
||||
|
||||
buffer.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimating stage combines multiple CIC stages with a decimator. The
|
||||
* number of stages is indicated by the order value and the size indicates
|
||||
* the decimation rate of this stage.
|
||||
*/
|
||||
public class DecimatingStage implements RealSampleListener
|
||||
{
|
||||
private ArrayList<Stage> mStages = new ArrayList<Stage>();
|
||||
private Stage mFirstStage;
|
||||
private RealDecimator mDecimator;
|
||||
|
||||
public DecimatingStage(int size, int order)
|
||||
{
|
||||
for(int x = 0; x < order; x++)
|
||||
{
|
||||
Stage stage;
|
||||
|
||||
if(size == 2)
|
||||
{
|
||||
stage = new TwoStage();
|
||||
}
|
||||
else
|
||||
{
|
||||
stage = new Stage(size);
|
||||
}
|
||||
|
||||
mStages.add(stage);
|
||||
|
||||
if(x == 0)
|
||||
{
|
||||
mFirstStage = stage;
|
||||
}
|
||||
else
|
||||
{
|
||||
mStages.get(x - 1).setListener(stage);
|
||||
}
|
||||
}
|
||||
|
||||
mDecimator = new RealDecimator(size);
|
||||
|
||||
mStages.get(mStages.size() - 1).setListener(mDecimator);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
for(Stage stage : mStages)
|
||||
{
|
||||
stage.dispose();
|
||||
}
|
||||
|
||||
mStages.clear();
|
||||
mDecimator.dispose();
|
||||
mDecimator = null;
|
||||
mFirstStage = null;
|
||||
mStages = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(float sample)
|
||||
{
|
||||
mFirstStage.receive(sample);
|
||||
}
|
||||
|
||||
public void setListener(RealSampleListener listener)
|
||||
{
|
||||
mDecimator.setListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single non-decimating CIC stage component. Uses a circular buffer and a
|
||||
* running average internally to implement the stage so that stage size has
|
||||
* essentially no impact on the computational requirements of the stage
|
||||
*/
|
||||
public class Stage implements RealSampleListener
|
||||
{
|
||||
protected RealSampleListener mListener;
|
||||
|
||||
private float[] mSamples;
|
||||
|
||||
protected float mSum;
|
||||
|
||||
private int mSamplePointer = 0;
|
||||
private int mSize;
|
||||
|
||||
protected float mGain;
|
||||
|
||||
public Stage()
|
||||
{
|
||||
}
|
||||
|
||||
public Stage(int size)
|
||||
{
|
||||
mSize = size - 1;
|
||||
|
||||
mSamples = new float[mSize];
|
||||
|
||||
mGain = 1.0f / (float)size;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void receive(float sample)
|
||||
{
|
||||
/* Subtract the oldest sample and add back in the newest */
|
||||
mSum = mSum - mSamples[mSamplePointer] + sample;
|
||||
|
||||
/* Overwrite the oldest sample with the newest */
|
||||
mSamples[mSamplePointer] = sample;
|
||||
|
||||
mSamplePointer++;
|
||||
|
||||
if(mSamplePointer >= mSize)
|
||||
{
|
||||
mSamplePointer = 0;
|
||||
}
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(mSum * mGain);
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(RealSampleListener listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Size 2 stage that removes the unnecessary circular buffer management for
|
||||
* a two-stage.
|
||||
*/
|
||||
public class TwoStage extends Stage
|
||||
{
|
||||
public TwoStage()
|
||||
{
|
||||
super();
|
||||
|
||||
mGain = 0.5f;
|
||||
}
|
||||
|
||||
public void receive(float sample)
|
||||
{
|
||||
float sum = mSum + sample;
|
||||
|
||||
mSum = sample;
|
||||
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(sum * mGain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output adapter - applies gain correction, applies cleanup filter, casts
|
||||
* double-valued samples into float-valued complex sample and sends the
|
||||
* result to the registered listener.
|
||||
*/
|
||||
public class Output implements RealSampleListener
|
||||
{
|
||||
/* Decimated output buffers will contain 1024 samples */
|
||||
private ReusableBufferAssembler mAssembler;
|
||||
private Listener<ReusableBuffer> mReusableBufferListener;
|
||||
|
||||
private RealFIRFilter2 mCleanupFilter;
|
||||
private HalfBandFilter mHalfBandFilter = new HalfBandFilter(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false);
|
||||
|
||||
public Output(int outputSampleRate, int passFrequency, int attenuation, WindowType windowType, int outputBufferSize)
|
||||
{
|
||||
mCleanupFilter = new RealFIRFilter2(FilterFactory.getCICCleanupFilter(outputSampleRate, passFrequency,
|
||||
attenuation, windowType), 0.4f);
|
||||
|
||||
mAssembler = new ReusableBufferAssembler(outputBufferSize);
|
||||
mAssembler.setListener(new Listener<ReusableBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(ReusableBuffer reusableBuffer)
|
||||
{
|
||||
|
||||
if(mReusableBufferListener != null)
|
||||
{
|
||||
ReusableBuffer cleanupFiltered = mCleanupFilter.filter(reusableBuffer);
|
||||
ReusableBuffer halfBandFiltered = mHalfBandFilter.filter(cleanupFiltered);
|
||||
mReusableBufferListener.receive(halfBandFiltered);
|
||||
}
|
||||
else
|
||||
{
|
||||
reusableBuffer.decrementUserCount();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mAssembler.dispose();
|
||||
mCleanupFilter.dispose();
|
||||
mHalfBandFilter.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver method for the output adapter to receive a filtered,
|
||||
* decimated sample, apply gain correction, apply cleanup filtering
|
||||
* and output the sample values as a complex sample.
|
||||
*/
|
||||
@Override
|
||||
public void receive(float sample)
|
||||
{
|
||||
mAssembler.receive(sample * 32.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive output samples
|
||||
*/
|
||||
public void setListener(Listener<ReusableBuffer> listener)
|
||||
{
|
||||
mReusableBufferListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving output samples
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mHalfBandFilter.removeListener();
|
||||
mReusableBufferListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.filter.correction;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
|
||||
public class IQCorrectionFilter
|
||||
{
|
||||
private float mRatio = 0.00001f;
|
||||
private float mAverageInphase = 0.0f;
|
||||
private float mAverageQuadrature = 0.0f;
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("IQCorrectionFilter");
|
||||
|
||||
public IQCorrectionFilter(float ratio)
|
||||
{
|
||||
mRatio = ratio;
|
||||
}
|
||||
|
||||
public IQCorrectionFilter()
|
||||
{
|
||||
}
|
||||
|
||||
public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
ReusableComplexBuffer filtered = mReusableComplexBufferQueue.getBuffer(samples.length);
|
||||
float[] filteredSamples = filtered.getSamples();
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
mAverageInphase = mAverageInphase + (mRatio * (samples[x] - mAverageInphase));
|
||||
filteredSamples[x] = samples[x] - mAverageInphase;
|
||||
|
||||
mAverageQuadrature = mAverageQuadrature + (mRatio * (samples[x + 1] - mAverageQuadrature));
|
||||
filteredSamples[x + 1] = samples[x + 1] - mAverageQuadrature;
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class BufferDCFilter
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BufferDCFilter.class);
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Buffer DC Filter");
|
||||
private double mAccumulator;
|
||||
|
||||
/**
|
||||
* Calculates the DC offset present in the samples and subtracts the offset from all samples.
|
||||
*/
|
||||
public void filterReal(float[] samples)
|
||||
{
|
||||
float adjustment = getOffset(samples);
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
samples[x] = samples[x] - adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
private float getOffset(float[] samples)
|
||||
{
|
||||
mAccumulator = 0.0;
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
mAccumulator += samples[x];
|
||||
}
|
||||
|
||||
return (float)(mAccumulator / (double)samples.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the DC offset present in the samples and subtracts the offset from all samples.
|
||||
*/
|
||||
public static void filterComplex(float[] samples)
|
||||
{
|
||||
double iAccumulator = 0.0;
|
||||
double qAccumulator = 0.0;
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
iAccumulator += samples[x];
|
||||
qAccumulator += samples[x + 1];
|
||||
}
|
||||
|
||||
double half = (double)samples.length / 2.0;
|
||||
|
||||
float iAdjustment = (float)(iAccumulator / half);
|
||||
float qAdjustment = (float)(qAccumulator / half);
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
samples[x] = samples[x] - iAdjustment;
|
||||
samples[x + 1] = samples[x + 1] - qAdjustment;
|
||||
}
|
||||
}
|
||||
|
||||
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
|
||||
{
|
||||
float[] unfilteredSamples = unfilteredBuffer.getSamples();
|
||||
|
||||
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length);
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
float offset = getOffset(unfilteredSamples);
|
||||
|
||||
for(int x = 0; x < unfilteredSamples.length; x++)
|
||||
{
|
||||
filteredSamples[x] = unfilteredSamples[x] - offset;
|
||||
}
|
||||
|
||||
unfilteredBuffer.decrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,96 @@
|
||||
/*******************************************************************************
|
||||
* 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/>
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
|
||||
/**
|
||||
* DC removal filter for removing any DC offset that may be present when a signal is not aligned within the
|
||||
* demodulated channel.
|
||||
*/
|
||||
public class DCRemovalFilter
|
||||
{
|
||||
protected float mAverage;
|
||||
protected float mRatio;
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("DC Removal Filter");
|
||||
protected float mAverage;
|
||||
protected float mRatio;
|
||||
|
||||
public DCRemovalFilter( float ratio )
|
||||
{
|
||||
mRatio = ratio;
|
||||
}
|
||||
/**
|
||||
* Creates a DC removal filter.
|
||||
*
|
||||
* @param ratio
|
||||
*/
|
||||
public DCRemovalFilter(float ratio)
|
||||
{
|
||||
mRatio = ratio;
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mAverage = 0.0f;
|
||||
}
|
||||
/**
|
||||
* Resets the running average to zero.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mAverage = 0.0f;
|
||||
}
|
||||
|
||||
public float filter( float sample )
|
||||
{
|
||||
float filtered = sample - mAverage;
|
||||
mAverage += mRatio * filtered;
|
||||
return filtered;
|
||||
}
|
||||
/**
|
||||
* Filters the sample and returns a filtered (ie DC-removed) sample.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public float filter(float sample)
|
||||
{
|
||||
float filtered = sample - mAverage;
|
||||
mAverage += mRatio * filtered;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the buffer samples and returns a new reusable buffer with the filtered samples.
|
||||
*
|
||||
* Note: this method automatically manages the user accounting between the buffer argument and the new returned
|
||||
* filtered sample buffer.
|
||||
*
|
||||
* @param unfilteredBuffer containing samples to filter
|
||||
* @return a new reusable buffer containing the filtered samples with the user count set to 1.
|
||||
*/
|
||||
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
|
||||
{
|
||||
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredBuffer.getSampleCount());
|
||||
|
||||
float[] unfilteredSamples = unfilteredBuffer.getSamples();
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < unfilteredSamples.length; x++)
|
||||
{
|
||||
filteredSamples[x] = filter(unfilteredSamples[x]);
|
||||
}
|
||||
|
||||
unfilteredBuffer.decrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
|
||||
public class DCRemovalFilter_CB implements Listener<ComplexBuffer>
|
||||
{
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
private DCRemovalFilter mIFilter;
|
||||
private DCRemovalFilter mQFilter;
|
||||
|
||||
public DCRemovalFilter_CB( float ratio )
|
||||
{
|
||||
mIFilter = new DCRemovalFilter( ratio );
|
||||
mQFilter = new DCRemovalFilter( ratio );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x += 2 )
|
||||
{
|
||||
samples[ x ] = mIFilter.filter( samples[ x ] );
|
||||
samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public ComplexBuffer filter( ComplexBuffer buffer )
|
||||
{
|
||||
filter( buffer.getSamples() );
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<ComplexBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class DCRemovalFilter_RB extends DCRemovalFilter
|
||||
implements Listener<RealBuffer>
|
||||
{
|
||||
protected Listener<RealBuffer> mListener;
|
||||
|
||||
public DCRemovalFilter_RB( float ratio )
|
||||
{
|
||||
super( ratio );
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
public RealBuffer filter( RealBuffer buffer )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
public class DCRemovalFilter_R_R extends DCRemovalFilter implements RealSampleListener
|
||||
{
|
||||
RealSampleListener mListener;
|
||||
|
||||
public DCRemovalFilter_R_R( float ratio )
|
||||
{
|
||||
super( ratio );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( sample ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class DCRemovalFilter_RealBuffer implements Listener<RealBuffer>
|
||||
{
|
||||
private Listener<RealBuffer> mListener;
|
||||
/**
|
||||
* Removes DC bias present across the samples in the buffer by calculating
|
||||
* the mean DC component and subtracting that component from each of the
|
||||
* samples.
|
||||
*/
|
||||
public DCRemovalFilter_RealBuffer()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
public RealBuffer filter( RealBuffer buffer )
|
||||
{
|
||||
double accumulator = 0.0d;
|
||||
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
accumulator += sample;
|
||||
}
|
||||
|
||||
float mean = (float)( accumulator / (double)buffer.getSamples().length );
|
||||
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
sample -= mean;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,105 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
public class IIRSinglePoleDCRemovalFilter implements RealSampleListener
|
||||
{
|
||||
private float mAlpha;
|
||||
private float mPreviousInput = 0.0f;
|
||||
private float mPreviousOutput = 0.0f;
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("IIR DC Filter");
|
||||
private float mAlpha;
|
||||
private float mPreviousInput = 0.0f;
|
||||
private float mPreviousOutput = 0.0f;
|
||||
private float mCurrentOutput = 0.0f;
|
||||
|
||||
private RealSampleListener mListener;
|
||||
|
||||
/**
|
||||
* IIR single-pole DC removal filter, as described by J M de Freitas in
|
||||
* 29Jan2007 paper at:
|
||||
*
|
||||
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
|
||||
*
|
||||
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
|
||||
* the cutoff frequency is to DC.
|
||||
*/
|
||||
public IIRSinglePoleDCRemovalFilter( float alpha )
|
||||
{
|
||||
mAlpha = alpha;
|
||||
}
|
||||
@Override
|
||||
public void receive( float currentInput )
|
||||
private RealSampleListener mListener;
|
||||
|
||||
/**
|
||||
* IIR single-pole DC removal filter, as described by J M de Freitas in
|
||||
* 29Jan2007 paper at:
|
||||
*
|
||||
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
|
||||
*
|
||||
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
|
||||
* the cutoff frequency is to DC.
|
||||
*/
|
||||
public IIRSinglePoleDCRemovalFilter(float alpha)
|
||||
{
|
||||
float currentOutput = ( currentInput - mPreviousInput ) +
|
||||
( mAlpha * mPreviousOutput );
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( currentOutput );
|
||||
}
|
||||
|
||||
mPreviousInput = currentInput;
|
||||
mPreviousOutput = currentOutput;
|
||||
mAlpha = alpha;
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
/**
|
||||
* Filters the sample
|
||||
* @param sample to filter
|
||||
* @return filtered output sample
|
||||
*/
|
||||
public float filter(float sample)
|
||||
{
|
||||
mCurrentOutput = (sample - mPreviousInput) + (mAlpha * mPreviousOutput);
|
||||
|
||||
mPreviousInput = sample;
|
||||
mPreviousOutput = mCurrentOutput;
|
||||
|
||||
return mCurrentOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the input buffer and returns a new filtered output buffer.
|
||||
*
|
||||
* Buffer user accounting is handled by this method where the input buffer user count is decremented and the
|
||||
* returned output buffer has the user count incremented to one.
|
||||
*
|
||||
* @param inputBuffer to filter
|
||||
* @return filtered samples in a new (ie reused) output buffer
|
||||
*/
|
||||
public ReusableBuffer filter(ReusableBuffer inputBuffer)
|
||||
{
|
||||
ReusableBuffer outputBuffer = mReusableBufferQueue.getBuffer(inputBuffer.getSampleCount());
|
||||
float[] inputSamples = inputBuffer.getSamples();
|
||||
float[] outputSamples = outputBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < inputSamples.length; x++)
|
||||
{
|
||||
outputSamples[x] = filter(inputSamples[x]);
|
||||
}
|
||||
|
||||
inputBuffer.decrementUserCount();
|
||||
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the RealSampleListener interface for single sample streams.
|
||||
*/
|
||||
@Override
|
||||
public void receive(float sample)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(filter(sample));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the listener to receive filtered samples processed by the RealSampleListener interface
|
||||
*/
|
||||
public void setListener(RealSampleListener listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.dc;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class IIRSinglePoleDCRemovalFilter_RB implements Listener<RealBuffer>
|
||||
{
|
||||
private float mAlpha;
|
||||
private float mPreviousInput = 0.0f;
|
||||
private float mPreviousOutput = 0.0f;
|
||||
private Listener<RealBuffer> mListener;
|
||||
|
||||
/**
|
||||
* IIR single-pole DC removal filter, as described by J M de Freitas in
|
||||
* 29Jan2007 paper at:
|
||||
*
|
||||
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
|
||||
*
|
||||
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
|
||||
* the cutoff frequency is to DC.
|
||||
*/
|
||||
public IIRSinglePoleDCRemovalFilter_RB( float alpha )
|
||||
{
|
||||
mAlpha = alpha;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
|
||||
public RealBuffer filter( RealBuffer buffer )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public float filter( float sample )
|
||||
{
|
||||
float currentOutput = ( sample - mPreviousInput ) +
|
||||
( mAlpha * mPreviousOutput );
|
||||
|
||||
mPreviousInput = sample;
|
||||
mPreviousOutput = currentOutput;
|
||||
|
||||
return currentOutput;
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mPreviousInput = 0.0f;
|
||||
mPreviousOutput = 0.0f;
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.design;
|
||||
|
||||
import io.github.dsheirer.gui.control.CurveFittedAreaChart;
|
||||
@@ -58,16 +73,6 @@ public class FilterView extends BorderPane
|
||||
mImpulseChart.setAlternativeColumnFillVisible(false);
|
||||
mImpulseChart.setAlternativeRowFillVisible(false);
|
||||
|
||||
URL cssURL = FilterView.class.getResource("FilterView.css");
|
||||
|
||||
String css = null;
|
||||
|
||||
if(cssURL != null)
|
||||
{
|
||||
css = cssURL.toExternalForm();
|
||||
|
||||
}
|
||||
|
||||
hbox.getChildren().add(mImpulseChart);
|
||||
HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
|
||||
|
||||
@@ -81,8 +86,12 @@ public class FilterView extends BorderPane
|
||||
mDFTChart.setAlternativeColumnFillVisible(false);
|
||||
mDFTChart.setAlternativeRowFillVisible(false);
|
||||
|
||||
if(css != null)
|
||||
|
||||
URL url = getClass().getResource("/../resources/FilterView.css");
|
||||
|
||||
if(url != null)
|
||||
{
|
||||
String css = url.toExternalForm();
|
||||
mImpulseChart.getStylesheets().add(css);
|
||||
mDFTChart.getStylesheets().add(css);
|
||||
}
|
||||
|
||||
@@ -40,52 +40,48 @@ public class FilterViewer extends Application
|
||||
private float[] getFilter()
|
||||
{
|
||||
// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
// .sampleRate(48000)
|
||||
// .sampleRate(8000)
|
||||
// .gridDensity(16)
|
||||
// .passBandCutoff(2500)
|
||||
// .oddLength(true)
|
||||
// .passBandCutoff(300)
|
||||
// .passBandAmplitude(1.0)
|
||||
// .passBandRipple(0.01)
|
||||
// .stopBandStart(4000)
|
||||
// .stopBandStart(500)
|
||||
// .stopBandAmplitude(0.0)
|
||||
// .stopBandRipple(0.008)
|
||||
// .stopBandRipple(0.03) //Approximately 60 dB attenuation
|
||||
// .build();
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(48000)
|
||||
.passBandCutoff(5100)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandStart(6500)
|
||||
.stopBandRipple(0.01)
|
||||
.build();
|
||||
|
||||
//
|
||||
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
// .sampleRate(24000)
|
||||
// .stopBandCutoff(800)
|
||||
// .sampleRate(8000)
|
||||
// .stopBandCutoff(200)
|
||||
// .stopBandAmplitude(0.0)
|
||||
// .stopBandRipple(0.03)
|
||||
// .passBandStart(1000)
|
||||
// .stopBandRipple(0.025)
|
||||
// .passBandStart(300)
|
||||
// .passBandAmplitude(1.0)
|
||||
// .passBandRipple(0.08)
|
||||
// .passBandRipple(0.01)
|
||||
// .build();
|
||||
|
||||
// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
|
||||
// .sampleRate( 48000 )
|
||||
// .stopFrequency1( 200 )
|
||||
// .passFrequencyBegin( 400 )
|
||||
// .passFrequencyEnd( 3200 )
|
||||
// .stopFrequency2( 3400 )
|
||||
// .stopRipple( 0.0003 )
|
||||
// .passRipple( 0.008)
|
||||
// .build();
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopFrequency1(1000)
|
||||
.passFrequencyBegin(1100)
|
||||
.passFrequencyEnd(1900)
|
||||
.stopFrequency2(2000)
|
||||
.stopRipple(0.000001)
|
||||
.passRipple(0.00001)
|
||||
.build();
|
||||
|
||||
float[] taps = null;
|
||||
|
||||
// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN);
|
||||
|
||||
// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
|
||||
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
|
||||
// Window.WindowType.HAMMING, true);
|
||||
// try
|
||||
// {
|
||||
// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19);
|
||||
// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
|
||||
// }
|
||||
// catch(FilterDesignException fde)
|
||||
// {
|
||||
// mLog.error("Error");
|
||||
// }
|
||||
|
||||
try
|
||||
{
|
||||
@@ -101,6 +97,12 @@ public class FilterViewer extends Application
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
throw new IllegalStateException("Couldn't design filter");
|
||||
}
|
||||
|
||||
|
||||
return taps;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,90 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.fir.complex;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
|
||||
public class ComplexFIRFilter extends FIRFilter
|
||||
{
|
||||
private RealFIRFilter mIFilter;
|
||||
private RealFIRFilter mQFilter;
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs. Wraps two real
|
||||
* FIR filters for processing each of the inphase and quadrature samples.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
||||
*/
|
||||
public ComplexFIRFilter(float[] coefficients, float gain)
|
||||
{
|
||||
mIFilter = new RealFIRFilter(coefficients, gain);
|
||||
mQFilter = new RealFIRFilter(coefficients, gain);
|
||||
}
|
||||
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mIFilter.getCoefficients();
|
||||
}
|
||||
|
||||
public float filterInphase(float sample)
|
||||
{
|
||||
return mIFilter.filter(sample);
|
||||
}
|
||||
|
||||
public float filterQuadrature(float sample)
|
||||
{
|
||||
return mQFilter.filter(sample);
|
||||
}
|
||||
|
||||
public Complex filter(Complex sample)
|
||||
{
|
||||
float i = filterInphase(sample.inphase());
|
||||
float q = filterQuadrature(sample.quadrature());
|
||||
|
||||
return new Complex(i, q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
|
||||
*
|
||||
* @param buffer with complex samples to filter
|
||||
* @return new buffer containing filtered complex samples
|
||||
*/
|
||||
public ComplexBuffer filter(ComplexBuffer buffer)
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
float[] filtered = new float[samples.length];
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
filtered[x] = filterInphase(samples[x]);
|
||||
filtered[x + 1] = filterQuadrature(samples[x + 1]);
|
||||
}
|
||||
|
||||
return new ComplexBuffer(filtered);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIFilter.dispose();
|
||||
mQFilter.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.fir.complex;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class ComplexFIRFilter2 extends FIRFilter
|
||||
{
|
||||
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Complex FIR Filter");
|
||||
private RealFIRFilter2 mIFilter;
|
||||
private RealFIRFilter2 mQFilter;
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter
|
||||
* instances.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
|
||||
*/
|
||||
public ComplexFIRFilter2(float[] coefficients, float gain)
|
||||
{
|
||||
mIFilter = new RealFIRFilter2(coefficients, gain);
|
||||
mQFilter = new RealFIRFilter2(coefficients, gain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex sample pairs that internally uses two RealFIRFilter
|
||||
* instances. This constructor uses a default gain of 1.0f.
|
||||
*
|
||||
* @param coefficients - filter taps
|
||||
*/
|
||||
public ComplexFIRFilter2(float[] coefficients)
|
||||
{
|
||||
this(coefficients, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the inphase sample value.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public float filterInphase(float sample)
|
||||
{
|
||||
return mIFilter.filter(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current filtered inphase sample value after invoking the filterInphase() method.
|
||||
*/
|
||||
public float currentInphaseValue()
|
||||
{
|
||||
return mIFilter.currentValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the quadrature sample value.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public float filterQuadrature(float sample)
|
||||
{
|
||||
return mQFilter.filter(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current filtered quadrature sample value after invoking the filterInphase() method.
|
||||
*/
|
||||
public float currentQuadratureValue()
|
||||
{
|
||||
return mQFilter.currentValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex sample.
|
||||
* @param sample to filter
|
||||
* @return filtered sample
|
||||
*/
|
||||
public Complex filter(Complex sample)
|
||||
{
|
||||
float i = filterInphase(sample.inphase());
|
||||
float q = filterQuadrature(sample.quadrature());
|
||||
|
||||
return new Complex(i, q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex samples from the reusable buffer and returns a new complex buffer with the filtered output
|
||||
*
|
||||
* Note: the original reusable buffer user count is decremented and a new reusable buffer is returned
|
||||
* with the user count already incremented to one.
|
||||
*
|
||||
* @param originalBuffer with complex samples to filter
|
||||
* @return new buffer containing filtered complex samples
|
||||
*/
|
||||
public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer)
|
||||
{
|
||||
ReusableComplexBuffer filteredBuffer = mReusableComplexBufferQueue.getBuffer(originalBuffer.getSamples().length);
|
||||
filteredBuffer.setTimestamp(originalBuffer.getTimestamp());
|
||||
|
||||
float[] samples = originalBuffer.getSamples();
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < originalBuffer.getSamples().length; x += 2)
|
||||
{
|
||||
filteredSamples[x] = filterInphase(samples[x]);
|
||||
filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
|
||||
}
|
||||
|
||||
originalBuffer.decrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mIFilter.dispose();
|
||||
mQFilter.dispose();
|
||||
mReusableComplexBufferQueue.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.fir.complex;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
|
||||
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener<ComplexBuffer>
|
||||
{
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
|
||||
/**
|
||||
* Complex FIR Filter for processing complex buffers. Wraps two real FIR
|
||||
* filters for processing each of the inphase and quadrature samples.
|
||||
*
|
||||
* @param coefficients - odd length symmetric filter taps
|
||||
* @param gain - gain value to apply to each of the filtered samples
|
||||
*/
|
||||
public ComplexFIRFilter_CB_CB( float[] coefficients, float gain )
|
||||
{
|
||||
super( coefficients, gain );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( ComplexBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for( int x = 0; x < samples.length; x += 2 )
|
||||
{
|
||||
samples[ x ] = filterInphase( samples[ x ] );
|
||||
samples[ x + 1 ] = filterQuadrature( samples[ x + 1 ] );
|
||||
}
|
||||
|
||||
mListener.receive( buffer );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<ComplexBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.fir.real;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
|
||||
public class RealFIRFilter extends FIRFilter
|
||||
{
|
||||
private float[] mBuffer;
|
||||
private int mBufferSize = 1;
|
||||
private int mBufferPointer = 0;
|
||||
private int[][] mIndexMap;
|
||||
|
||||
private float[] mCoefficients;
|
||||
private float mGain;
|
||||
|
||||
/**
|
||||
* Float sample FIR filter base class.
|
||||
*
|
||||
* @param coefficients - filter coefficients
|
||||
* @param gain value to apply to the filtered output - use 1.0f for no gain
|
||||
*/
|
||||
public RealFIRFilter( float[] coefficients, float gain )
|
||||
{
|
||||
mCoefficients = coefficients;
|
||||
mGain = gain;
|
||||
|
||||
mBufferSize = mCoefficients.length;
|
||||
|
||||
mBuffer = new float[ mBufferSize ];
|
||||
mBufferPointer = mBufferSize - 1;
|
||||
|
||||
generateIndexMap( mBufferSize );
|
||||
}
|
||||
|
||||
public float[] getCoefficients()
|
||||
{
|
||||
return mCoefficients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mCoefficients = null;
|
||||
mIndexMap = null;
|
||||
mBuffer = null;
|
||||
}
|
||||
|
||||
public float filter( float sample )
|
||||
{
|
||||
mBuffer[ mBufferPointer ] = sample;
|
||||
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for( int x = 0; x < mBufferSize; x++ )
|
||||
{
|
||||
accumulator += mCoefficients[ x ] *
|
||||
mBuffer[ mIndexMap[ mBufferPointer][ x ] ];
|
||||
}
|
||||
|
||||
mBufferPointer--;
|
||||
|
||||
if( mBufferPointer < 0 )
|
||||
{
|
||||
mBufferPointer += mBufferSize;
|
||||
}
|
||||
|
||||
/* Apply gain and return the filtered value */
|
||||
return accumulator * mGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a circular buffer index map to support lookup of the translated
|
||||
* index based on the current buffer pointer and the desired sample index.
|
||||
*/
|
||||
private void generateIndexMap( int size )
|
||||
{
|
||||
mIndexMap = new int[ size ][ size ];
|
||||
|
||||
for( int x = 0; x < size; x++ )
|
||||
{
|
||||
for( int y = 0; y < size; y++ )
|
||||
{
|
||||
int z = x + y;
|
||||
|
||||
mIndexMap[ x ][ y ] = z < size ? z : z - size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.filter.fir.real;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
|
||||
/**
|
||||
* Finite Impulse Response (FIR) filter for filtering individual float samples or float sample arrays.
|
||||
*
|
||||
* Note: filtering operations in this class are structured to leverage SIMD processor intrinsics when
|
||||
* available to the Java runtime.
|
||||
*/
|
||||
public class RealFIRFilter2 extends FIRFilter
|
||||
{
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("RealFIRFilter2");
|
||||
|
||||
private float[] mData;
|
||||
private float[] mCoefficients;
|
||||
private float mGain;
|
||||
private float mAccumulator;
|
||||
|
||||
/**
|
||||
* Float sample FIR filter base class.
|
||||
*
|
||||
* @param coefficients - filter coefficients in normal order.
|
||||
* @param gain value to apply to the filtered output. Use 1.0f for no gain
|
||||
*/
|
||||
public RealFIRFilter2(float[] coefficients, float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
mCoefficients = coefficients;
|
||||
mData = new float[coefficients.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Float sample FIR filter base class that uses a default gain value of 1.0f.
|
||||
*
|
||||
* @param coefficients - filter coefficients in normal order.
|
||||
*/
|
||||
public RealFIRFilter2(float[] coefficients)
|
||||
{
|
||||
this(coefficients, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes this filter to prepare for garbage collection.
|
||||
*/
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mCoefficients = null;
|
||||
mData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the sample argument. Loads the sample into the internal data buffer and performs
|
||||
* convolution between the sample buffer and the filter coefficients.
|
||||
*
|
||||
* @param sample to load
|
||||
* @return filtered value
|
||||
*/
|
||||
public float filter(float sample)
|
||||
{
|
||||
//Use array copy to leverage SIMD intrinsics
|
||||
System.arraycopy(mData, 0, mData, 1, mData.length - 1);
|
||||
mData[0] = sample;
|
||||
|
||||
mAccumulator = 0.0f;
|
||||
|
||||
//Use vector dot product to leverage SIMD intrinsics
|
||||
for(int x = 0; x < mCoefficients.length; x++)
|
||||
{
|
||||
mAccumulator += mData[x] * mCoefficients[x];
|
||||
}
|
||||
|
||||
/* Apply gain and return the filtered value */
|
||||
mAccumulator *= mGain;
|
||||
|
||||
return mAccumulator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current filtered output value for the filter after the filter() method has been invoked.
|
||||
*/
|
||||
public float currentValue()
|
||||
{
|
||||
return mAccumulator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filters the samples contained in the unfilteredBuffer and returns a new reusable buffer with the
|
||||
* filtered samples.
|
||||
*
|
||||
* Note: user count on the returned (new) buffer is set to one and the user count is decremented on
|
||||
* the unfiltered buffer argument.
|
||||
*
|
||||
* @param unfilteredBuffer containing a sample array to be filtered
|
||||
* @return a new reusable buffer with the filtered samples.
|
||||
*/
|
||||
public ReusableBuffer filter(ReusableBuffer unfilteredBuffer)
|
||||
{
|
||||
float[] unfilteredSamples = unfilteredBuffer.getSamples();
|
||||
|
||||
ReusableBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length);
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < unfilteredSamples.length; x++)
|
||||
{
|
||||
filteredSamples[x] = filter(unfilteredSamples[x]);
|
||||
}
|
||||
|
||||
unfilteredBuffer.decrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package io.github.dsheirer.dsp.filter.fir.real;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class RealFIRFilter_RB_RB extends RealFIRFilter
|
||||
implements Listener<RealBuffer>
|
||||
{
|
||||
private Listener<RealBuffer> mListener;
|
||||
|
||||
/**
|
||||
* Real Buffer Float FIR filter with streaming float buffer provider and
|
||||
* listener interfaces.
|
||||
*
|
||||
* @param coefficients
|
||||
* @param gain
|
||||
*/
|
||||
public RealFIRFilter_RB_RB( float[] coefficients, float gain )
|
||||
{
|
||||
super( coefficients, gain );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( filter( buffer ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the buffer samples
|
||||
*
|
||||
* @param buffer
|
||||
* @return
|
||||
*/
|
||||
public RealBuffer filter( RealBuffer buffer )
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
{
|
||||
samples[ x ] = filter( samples[ x ] );
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener()
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user