diff --git a/copyright_header.txt b/copyright_header.txt deleted file mode 100644 index 3eaea8b8..00000000 --- a/copyright_header.txt +++ /dev/null @@ -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 - ******************************************************************************/ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java index 1210e45a..8565d3ed 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ChannelCalculator.java @@ -185,7 +185,7 @@ public class ChannelCalculator */ public double getChannelSampleRate() { - return (double)getChannelBandwidth() * getOversampling(); + return getChannelBandwidth() * getOversampling(); } /** @@ -550,9 +550,9 @@ public class ChannelCalculator { mLog.debug("Starting ...."); - int channelBandwidth = 12500; - int sampleRate = 100000; //Airspy - int channelCount = sampleRate / channelBandwidth; + double channelBandwidth = 12500.0; + double sampleRate = 100000.0; //Airspy + int channelCount = (int)(sampleRate / channelBandwidth); double centerFrequency = 455000000; ChannelCalculator calculator = new ChannelCalculator(sampleRate, channelCount, centerFrequency, 2.0); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index 3e0995dc..d15f0880 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -59,7 +59,7 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann if(channelCount % 2 != 0) { - throw new IllegalArgumentException("Channel count must be an even multiple of the oversample rate (2)"); + throw new IllegalArgumentException("Channel count must be an even multiple of the oversample rate (2x)"); } init(taps); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 2d673111..a7761217 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -1,6 +1,6 @@ /******************************************************************************* * sdr-trunk - * Copyright (C) 2014-2017 Dennis Sheirer + * 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 @@ -19,26 +19,31 @@ import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputP 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.complex.IComplexBufferProvider; -import io.github.dsheirer.source.Source; +import io.github.dsheirer.source.ISourceEventProcessor; 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.List; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; -public class PolyphaseChannelManager implements Listener +public class PolyphaseChannelManager implements ISourceEventProcessor { private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); - private static final int CHANNEL_BANDWIDTH = 12500; + private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.0; private static final double CHANNEL_OVERSAMPLING = 2.0; - private static final int CHANNEL_SAMPLE_RATE = (int)(CHANNEL_BANDWIDTH * CHANNEL_OVERSAMPLING); private static final int POLYPHASE_FILTER_TAPS_PER_CHANNEL = 17; + private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); private IComplexBufferProvider mComplexBufferProvider; private List mChannelSources = new CopyOnWriteArrayList<>(); private ChannelCalculator mChannelCalculator; @@ -61,7 +66,7 @@ public class PolyphaseChannelManager implements Listener * @param frequency of the provided complex buffer samples * @param sampleRate of the provided complex buffer samples */ - public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, int sampleRate) + public PolyphaseChannelManager(IComplexBufferProvider complexBufferProvider, long frequency, double sampleRate) { if(complexBufferProvider == null) { @@ -70,14 +75,13 @@ public class PolyphaseChannelManager implements Listener mComplexBufferProvider = complexBufferProvider; - //Ensure that sample rate is a multiple of channel sample rate since polyphase channelizer is M2 - if(sampleRate % (CHANNEL_SAMPLE_RATE) != 0) - { - throw new IllegalArgumentException("Tuner sample rate [" + sampleRate + "] must be an even multiple of " + - CHANNEL_SAMPLE_RATE + " Hz"); - } + int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH); - int channelCount = sampleRate / 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); @@ -86,13 +90,37 @@ public class PolyphaseChannelManager implements Listener updateChannelizer(sampleRate, null); } + /** + * Creates a polyphase channel manager for the tuner controller + */ + 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 Source getChannel(TunerChannel tunerChannel) + public TunerChannelSource getChannel(TunerChannel tunerChannel) { PolyphaseChannelSource channelSource = null; @@ -105,7 +133,7 @@ public class PolyphaseChannelManager implements Listener if(outputProcessor != null) { channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener, - CHANNEL_SAMPLE_RATE); + mChannelCalculator.getChannelSampleRate()); mChannelSources.add(channelSource); } @@ -131,9 +159,9 @@ public class PolyphaseChannelManager implements Listener switch(indexes.size()) { case 1: - return new OneChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes); case 2: - return new TwoChannelOutputProcessor(CHANNEL_SAMPLE_RATE, indexes); + return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes); 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()); @@ -152,6 +180,7 @@ public class PolyphaseChannelManager implements Listener synchronized(mBufferProcessor) { mPolyphaseChannelizer.addChannel(channelSource); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1) { @@ -172,6 +201,7 @@ public class PolyphaseChannelManager implements Listener synchronized(mBufferProcessor) { mChannelSources.remove(channelSource); + mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(mChannelSources.size())); if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0) { @@ -182,11 +212,11 @@ public class PolyphaseChannelManager implements Listener } /** - * Implements the Listener interface for receiving notifications of frequency and sample rate + * Implements the ISourceEventProcessor interface for receiving notifications of frequency and sample rate * changes from the tuner. */ @Override - public void receive(SourceEvent sourceEvent) + public void process(SourceEvent sourceEvent) { switch(sourceEvent.getEvent()) { @@ -199,6 +229,9 @@ public class PolyphaseChannelManager implements Listener //Update the channelizer (and the output processors) for the new sample rate updateChannelizer(sourceEvent.getValue().intValue(), sourceEvent); break; + default: + mLog.info("Received an unrecognized source event: " + sourceEvent.getEvent()); + break; } } @@ -210,7 +243,7 @@ public class PolyphaseChannelManager implements Listener * event will be passed to all output processors to ensure the change is fully promulgated before restarting the * buffer processor if it was already running. */ - private void updateChannelizer(int sampleRate, SourceEvent sourceEvent) + private void updateChannelizer(double sampleRate, SourceEvent sourceEvent) { mChannelCalculator.setSampleRate(sampleRate); @@ -322,6 +355,46 @@ public class PolyphaseChannelManager implements Listener } } + /** + * Sorted set of currently sourced tuner channels being provided by this channel manager. The set is ordered by + * frequency (low to high). + */ + public SortedSet getTunerChannels() + { + SortedSet 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 listener) + { + mSourceEventBroadcaster.addListener(listener); + } + + /** + * Removes the listener from receiving source events + */ + public void removeSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.removeListener(listener); + } + /** * Internal class for handling requests for start/stop sample stream from polyphase channel sources */ diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java index 350567a3..23c3e120 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/TwoChannelOutputProcessor.java @@ -38,7 +38,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor * @param sampleRate of the output sample stream. * @param channelIndexes containing two channel indices. */ - public TwoChannelOutputProcessor(int sampleRate, List channelIndexes) + public TwoChannelOutputProcessor(double sampleRate, List channelIndexes) { //Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency //after synthesizing both input channels diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java index 8f9b2857..53ef609f 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -18,15 +18,17 @@ ******************************************************************************/ package io.github.dsheirer.source; +import java.util.EnumSet; + public class SourceEvent { public enum Event { + NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE, NOTIFICATION_FREQUENCY_CHANGE, NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, - NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE, REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, @@ -34,6 +36,11 @@ public class SourceEvent REQUEST_SOURCE_DISPOSE, REQUEST_START_SAMPLE_STREAM, REQUEST_STOP_SAMPLE_STREAM; + + public static EnumSet NOTIFICATION_EVENTS = + EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); + public static EnumSet REQUEST_EVENTS = + EnumSet.range(NOTIFICATION_CHANNEL_COUNT_CHANGE, NOTIFICATION_SAMPLE_RATE_CHANGE); } private Event mEvent; @@ -66,6 +73,22 @@ public class SourceEvent return mEvent; } + /** + * Indicates if this is a notification event + */ + public boolean isNotificationEvent() + { + return Event.NOTIFICATION_EVENTS.contains(mEvent); + } + + /** + * Indicates if this is a request event + */ + public boolean isRequestEvent() + { + return Event.REQUEST_EVENTS.contains(mEvent); + } + /** * Value associated with the event. */ @@ -152,7 +175,7 @@ public class SourceEvent */ public static SourceEvent channelCountChange(int channelCount) { - return new SourceEvent(Event.NOTIFICATION_POLYPHASE_CHANNEL_COUNT_CHANGE, channelCount); + return new SourceEvent(Event.NOTIFICATION_CHANNEL_COUNT_CHANGE, channelCount); } /** @@ -199,4 +222,9 @@ public class SourceEvent { return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source); } + + public String toString() + { + return "SOURCE EVENT:" + mEvent + " VALUE:" + (mValue != null ? mValue : "(empty)"); + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java index 70e02d71..d9a19e90 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java @@ -1,19 +1,17 @@ /******************************************************************************* - * 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 + * 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 + * ******************************************************************************/ package io.github.dsheirer.source.tuner; @@ -24,20 +22,17 @@ import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.TunerEvent.Event; import io.github.dsheirer.source.tuner.manager.AbstractSourceManager; import io.github.dsheirer.source.tuner.manager.TunerSourceManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Tuner - provides tuner channel sources, representing a channel frequency + * Tuner provides an interface to a software or hardware tuner controller that provides I/Q sample data coupled with an + * abstract source manager to provide access to Digital Drop Channel (DDC) resources. */ -public abstract class Tuner +public abstract class Tuner implements ISourceEventProcessor { - private final static Logger mLog = LoggerFactory.getLogger(Tuner.class); - - protected Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); + private Broadcaster mTunerEventBroadcaster = new Broadcaster<>(); private TunerController mTunerController; + private AbstractSourceManager mSourceManager; private String mName; - private AbstractSourceManager mTunerSourceManager; /** * Abstract tuner class. @@ -48,28 +43,31 @@ public abstract class Tuner { mName = name; mTunerController = tunerController; + //Register to receive frequency and sample rate change notifications + mTunerController.addListener(this::process); - mTunerSourceManager = new TunerSourceManager(mTunerController); + mSourceManager = new TunerSourceManager(mTunerController); + //Register to receive channel count change notifications + mSourceManager.addSourceEventListener(this::process); + } - //Rebroadcast frequency and sample rate change events as tuner events - mTunerController.addListener(new ISourceEventProcessor() + @Override + public void process(SourceEvent event) + { + switch(event.getEvent()) { - @Override - public void process(SourceEvent event) - { - switch(event.getEvent()) - { - case NOTIFICATION_FREQUENCY_CHANGE: - broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY)); - break; - case NOTIFICATION_SAMPLE_RATE_CHANGE: - broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE)); - break; - default: - break; - } - } - }); + case NOTIFICATION_CHANNEL_COUNT_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.CHANNEL_COUNT)); + break; + case NOTIFICATION_FREQUENCY_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY)); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE)); + break; + default: + break; + } } /** @@ -77,14 +75,20 @@ public abstract class Tuner */ public AbstractSourceManager getSourceManager() { - return mTunerSourceManager; + return mSourceManager; } + /** + * Tuner controller. + */ public TunerController getTunerController() { return mTunerController; } + /** + * Name for this tuner + */ public String toString() { return mName; @@ -94,19 +98,19 @@ public abstract class Tuner { } + /** + * Sets the name for this tuner + */ public void setName(String name) { mName = name; } /** - * Unique identifier for this tuner, used to lookup a tuner configuration - * from the settings manager. + * Unique identifier for this tuner, used to lookup a tuner configuration from the settings manager. * - * @return - unique identifier like a serial number, or a usb bus location - * or ip address and port. Return some form of unique identification that - * allows this tuner to be identified from among several of the same types - * of tuners. + * @return - unique identifier like a serial number, or a usb bus location or ip address and port. Return some + * form of unique identification that allows this tuner to be identified from among the same types of tuners. */ public abstract String getUniqueID(); @@ -154,7 +158,7 @@ public abstract class Tuner /** * Broadcasts the tuner change event */ - public void broadcast(TunerEvent tunerEvent) + protected void broadcast(TunerEvent tunerEvent) { mTunerEventBroadcaster.broadcast(tunerEvent); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 0076862f..688ee442 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -26,29 +26,19 @@ import io.github.dsheirer.source.ISourceEventProcessor; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceEventListenerToProcessorAdapter; import io.github.dsheirer.source.SourceException; -import io.github.dsheirer.source.tuner.channel.CICTunerChannelSource; -import io.github.dsheirer.source.tuner.channel.TunerChannel; -import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.frequency.FrequencyController; import io.github.dsheirer.source.tuner.frequency.FrequencyController.Tunable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.SortedSet; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.RejectedExecutionException; - public abstract class TunerController implements Tunable, ISourceEventProcessor, ISourceEventListener, IComplexBufferProvider, Listener { private final static Logger mLog = LoggerFactory.getLogger(TunerController.class); protected Broadcaster mSampleBroadcaster = new Broadcaster<>(); - - /* List of currently tuned channels being served to demod channels */ - private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); protected FrequencyController mFrequencyController; - private int mMiddleUnusable; + private int mMiddleUnusableHalfBandwidth; private double mUsableBandwidthPercentage; private Listener mSourceEventListener; @@ -58,15 +48,15 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, * * @param minimumFrequency minimum uncorrected tunable frequency * @param maximumFrequency maximum uncorrected tunable frequency - * @param middleUnusable is the +/- value center DC spike to avoid for channels + * @param middleUnusableHalfBandwidth is the +/- value center DC spike to avoid for channels * @param usableBandwidth percentage of usable bandwidth relative to space at the extreme ends of the spectrum * @throws SourceException - for any issues related to constructing the * class, tuning a frequency, or setting the bandwidth */ - public TunerController(long minimumFrequency, long maximumFrequency, int middleUnusable, double usableBandwidth) + public TunerController(long minimumFrequency, long maximumFrequency, int middleUnusableHalfBandwidth, double usableBandwidth) { mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, 0.0d); - mMiddleUnusable = middleUnusable; + mMiddleUnusableHalfBandwidth = middleUnusableHalfBandwidth; mUsableBandwidthPercentage = usableBandwidth; mSourceEventListener = new SourceEventListenerToProcessorAdapter(this); } @@ -182,218 +172,39 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, } /** - * Indicates if channel along with all of the other currently sourced - * channels can fit within the tunable bandwidth. + * Total bandwidth of the middle unusable bandwidth region. This value is used to avoid a central DC spike + * present in some tuners. */ - private boolean canTune(TunerChannel channel) + public int getMiddleUnusableHalfBandwidth() { - //Make sure we're within the tunable frequency range of this tuner - if(getMinFrequency() < channel.getMinFrequency() && getMaxFrequency() > channel.getMaxFrequency()) - { - //If this is the first lock, then we're good - if(mTunedChannels.isEmpty()) - { - return true; - } - else - { - int usableBandwidth = getUsableBandwidth(); - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Requested channel is within current locked channel frequency range - if(minLockedFrequency <= channel.getMinFrequency() && channel.getMaxFrequency() <= maxLockedFrequency) - { - return true; - } - - //Requested channel is higher than min locked frequency - if(channel.getMaxFrequency() > minLockedFrequency && - channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth) - { - return true; - } - - //Requested channel is lower than the max locked frequency - if(channel.getMinFrequency() <= maxLockedFrequency && - maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth) - { - return true; - } - } - } - - return false; + return mMiddleUnusableHalfBandwidth; } /** - * Constructs a digital drop channel (DDC) as a tuner channel from from the tuner, or returns null if the channel - * cannot be sourced from the tuner. - * - * This controller can't provide a channel if the channel, along with the current set of sourced channels, can't be - * accomodated within the current tuner bandwidth, or if a center frequency cannot be calculated that will ensure - * all of the channels can be accomodated and any defined central DC spike avoided. - * - * @param tuner to source the channel from - * @param channel with defined center frequency and bandwidth - * @return fully constructed tuner channel or null - * @throws RejectedExecutionException if the decimation processor has an error + * Indicates if this tuner controller has a middle unusable bandwidth region. */ - public TunerChannelSource getChannel(Tuner tuner, TunerChannel channel) throws RejectedExecutionException + public boolean hasMiddleUnusableBandwidth() { - TunerChannelSource source = null; - - if(canTune(channel)) - { - try - { - mTunedChannels.add(channel); - - if(requiresLOUpdate(channel)) - { - updateLOFrequency(); - } - - source = new CICTunerChannelSource(tuner, channel); - } - catch(SourceException se) - { - mTunedChannels.remove(channel); - source = null; - } - } - - return source; - } - - public int getChannelCount() - { - return mTunedChannels.size(); - } - - /** - * Indicates if the tuner's LO frequency must be updated in order to accommodate the tuner channel - */ - private boolean requiresLOUpdate(TunerChannel channel) throws SourceException - { - boolean isBelow = channel.getMinFrequency() < getMinTunedFrequency(); - boolean isAbove = channel.getMaxFrequency() > getMaxTunedFrequency(); - boolean overlapsCenter = mMiddleUnusable > 0 && - channel.overlaps(getFrequency() - mMiddleUnusable, getFrequency() + mMiddleUnusable); - - return isAbove || isBelow || overlapsCenter; - } - - /** - * Releases the currently sourced tuner channel from this tuner and shuts down the tuner if no other sources exist. - */ - public void releaseChannel(TunerChannelSource tunerChannelSource) - { - if(tunerChannelSource != null) - { - mTunedChannels.remove(tunerChannelSource.getTunerChannel()); - } - } - - /** - * Sets the Local Oscillator frequency to accomodate the current set of tuned channels. - * - * If there is only a single tuned channel, it is placed immediately to the right of the usable bandwidth right - * of the central DC spike. - * - * Otherwise, it places the highest channel frequency at the upper end of the tuner bandwidth, and then iteratively - * moves the center frequency higher until all channels fit within the bandwidth and none of the channels overlap - * any defined central DC spike unusable region. If a center tune frequency cannot be calculated, throw an - * exception so that the most recently added channel can be removed. - * - * Note: the tuned frequency is not changed until a legitimate new frequency can be calculated. If an exception - * is thrown, the current frequency is retained, so that the recently added channel can be removed and all other - * channels can continue as previously arranged. - * - * @throws SourceException if the set of tuner channels, including a recently added channel, cannot be tuned - * within the current tuner bandwidth and any central DC spike unusable region. - */ - private void updateLOFrequency() throws SourceException - { - long candidateFrequency; - - boolean isValidCandidateFrequency = true; - - //If there is only 1 channel set the center frequency so that the channel is positioned to the right of center - if(mTunedChannels.size() == 1) - { - candidateFrequency = mTunedChannels.first().getMinFrequency() - mMiddleUnusable + 1; - } - else - { - long minLockedFrequency = mTunedChannels.first().getMinFrequency(); - long maxLockedFrequency = mTunedChannels.last().getMaxFrequency(); - - //Start by placing the highest frequency channel at the high end of the spectrum - candidateFrequency = maxLockedFrequency - (getUsableBandwidth() / 2); - - //Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists - if(mMiddleUnusable > 0) - { - boolean processingRequired = true; - - while(isValidCandidateFrequency && processingRequired) - { - processingRequired = false; - - long minAvoid = candidateFrequency - mMiddleUnusable; - long maxAvoid = candidateFrequency + mMiddleUnusable; - - //If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively - //increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until - //we either find a good center tune frequency, or we walk the lowest frequency channel out of the - //minimum tuned range, in which case we'll throw an exception indicating we don't have a solution. - for(TunerChannel channel : mTunedChannels) - { - if(channel.overlaps(minAvoid, maxAvoid)) - { - //Calculate a tuned frequency adjustment that places this overlapping channel just to the - //left of the central DC spike avoid zone - long adjustment = channel.getMaxFrequency() - minAvoid + 1; - - //If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment - if(candidateFrequency + adjustment - (getUsableBandwidth() / 2) <= minLockedFrequency) - { - candidateFrequency += adjustment; - processingRequired = true; - } - //Otherwise, punt and indicate that we can't find a center frequency or add the channel - else - { - isValidCandidateFrequency = false; - } - - //break out of the for/each loop, so that we can start over again with all of the channels - break; - } - } - } - } - } - - if(isValidCandidateFrequency) - { - mFrequencyController.setFrequency(candidateFrequency); - } - else - { - throw new SourceException("Couldn't calculate viable center frequency from set of tuner channels"); - } + return mMiddleUnusableHalfBandwidth != 0; } /** * Usable bandwidth - total bandwidth minus the unusable space at either end of the spectrum. */ - private int getUsableBandwidth() + public int getUsableBandwidth() { return (int)(getBandwidth() * mUsableBandwidthPercentage); } + /** + * Usable half bandwidth - total bandwidth minus unusable space at either end of the spectrum, minus the blocked + * center bandwidth. + */ + public int getUsableHalfBandwidth() + { + return (int)(getUsableBandwidth() / 2) - mMiddleUnusableHalfBandwidth; + } + /** * Sets the listener to be notified any time that the tuner changes frequency * or bandwidth/sample rate. diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java index a0735a24..919f32d6 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java @@ -235,7 +235,7 @@ public class TunerModel extends AbstractTableModel implements Listener +public abstract class AbstractSourceManager implements IComplexBufferProvider, ISourceEventProcessor { - protected Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); + private Broadcaster mSourceEventBroadcaster = new Broadcaster<>(); /** - * Indicates if this source manager can tune (ie provide a source for) the specified tuner channel + * Sorted set of tuner channels being sourced by this source manager. Set is ordered by frequency lowest to highest */ - public abstract boolean canTune(TunerChannel tunerChannel); + public abstract SortedSet getTunerChannels(); /** - * List of tuner channels being sourced by this source manager + * Count of tuner channels being sourced by this source manager. + * @return */ - public abstract List getChannels(); + public abstract int getTunerChannelCount(); /** * Obtains a source for the tuner channel or returns null if the channel cannot be sourced by this tuner. - * Note: use the canTune() method to check if a tuner channel can be sourced/tuned prior to accessing this method. + * + * Note: you MUST invoke start() on the obtained source to start the sample flow and invoke stop() to release all + * resources allocated for the tuner channel source. * * @param tunerChannel for requested source * @return tuner channel source or null */ public abstract TunerChannelSource getSource(TunerChannel tunerChannel); - - /** - * Releases the source and all related resources - */ - public abstract void releaseSource(TunerChannelSource source); - - /** - * Adds the listener to receive source events from this source manager - * - * @param sourceEventListener to add - */ - public abstract void addSourceEventListener(Listener sourceEventListener); - - /** - * Removes the listener from receiving source events from this source manager - * @param sourceEventListener to remove - */ - public abstract void removeSourceEventListener(Listener sourceEventListener); - /** * Adds the listener to receive complex buffer samples */ @@ -105,4 +91,28 @@ public abstract class AbstractSourceManager implements IComplexBufferProvider, L { mComplexBufferBroadcaster.broadcast(complexBuffer); } + + /** + * Adds a listener to receive source events + */ + public void addSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.addListener(listener); + } + + /** + * Remove the listener from receiving source events + */ + public void removeSourceEventListener(Listener listener) + { + mSourceEventBroadcaster.removeListener(listener); + } + + /** + * Broadcasts the source event to any registered listeners + */ + protected void broadcast(SourceEvent sourceEvent) + { + mSourceEventBroadcaster.broadcast(sourceEvent); + } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java index 8b7c913d..9276f649 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerSourceManager.java @@ -1,6 +1,6 @@ /******************************************************************************* * sdr-trunk - * Copyright (C) 2014-2017 Dennis Sheirer + * 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 @@ -16,65 +16,405 @@ package io.github.dsheirer.source.tuner.manager; import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelManager; -import io.github.dsheirer.sample.Listener; 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.List; +import java.util.SortedSet; public class TunerSourceManager extends AbstractSourceManager { + private final static Logger mLog = LoggerFactory.getLogger(TunerSourceManager.class); private PolyphaseChannelManager mPolyphaseChannelManager; private TunerController mTunerController; + /** + * TunerSourceManager is responsible for managing the tuner's center tuned frequency and providing access to + * polyphase tuner channel sources (ie DDCs). This class is responsible for determining IF a requested channel can + * be provided and then adjusting the center frequency and provisioning a DDC Polyphase Tuner channel source. + * + * @param tunerController with a center tuned frequency that will be managed by this instance + */ public TunerSourceManager(TunerController tunerController) { mTunerController = tunerController; + //Register to receive frequency and sample rate notifications + mTunerController.addListener(this); + + mPolyphaseChannelManager = new PolyphaseChannelManager(tunerController); + //Register to receive channel count change notifications for rebroadcasting + mPolyphaseChannelManager.addSourceEventListener(this::process); } - //TODO: broadcast a tuner event each time we add a new channel to the polyphase manager - - @Override - public boolean canTune(TunerChannel tunerChannel) + /** + * Indicates if the channel min/max frequencies are within the tunable frequency range of the tuner controller + * + * @param tunerChannel to check + * @return true if the channel is within the frequency range of the tuner controller + */ + private boolean isTunable(TunerChannel tunerChannel) { + return mTunerController.canTune(tunerChannel.getMinFrequency()) && + mTunerController.canTune(tunerChannel.getMaxFrequency()); + } + + /** + * Indicates if the list of tuner channels can be tuned by the tuner controller within the current bandwidth and + * tuner center-blocked frequency range limitations. + * + * @param tunerChannels that are being evaluated + * @return true if the set of channels fit within the bandwidth and center blocked frequency band of the tuner + */ + private boolean canTune(SortedSet tunerChannels) + { + //If there is only one channel, check to ensure it fits the current bandwidth or half-bandwidth + if(tunerChannels.size() == 1) + { + if(mTunerController.hasMiddleUnusableBandwidth()) + { + return tunerChannels.first().getBandwidth() < mTunerController.getUsableHalfBandwidth(); + } + else + { + return tunerChannels.first().getBandwidth() < mTunerController.getUsableBandwidth(); + } + } + else + { + //Check that the total bandwidth of the channel set is within the usable bandwidth of the tuner + int tunerUsableBandwidth = mTunerController.getUsableBandwidth(); + + long channelSetBandwidth = tunerChannels.last().getMaxFrequency() - tunerChannels.first().getMinFrequency(); + + if(channelSetBandwidth <= tunerUsableBandwidth) + { + //If the tuner doesn't have a center DC spike blocked region, we're good + if(!mTunerController.hasMiddleUnusableBandwidth()) + { + return true; + } + + //Check to see if the channel set fits within the usable half bandwidth of the tuner + int tunerUsableHalfBandwidth = mTunerController.getUsableHalfBandwidth(); + + if(channelSetBandwidth < tunerUsableHalfBandwidth) + { + return true; + } + + //At this point we're within the tuner's total usable bandwidth, but there is a central DC spike blocked + //region. See if we can notionally partition the channels into left/right usable half bandwidth regions + TunerChannel firstRightChannel = null; + + for(TunerChannel tunerChannel: tunerChannels) + { + if(firstRightChannel == null) + { + long leftSideBandwidth = tunerChannel.getMaxFrequency() - tunerChannels.first().getMinFrequency(); + + if(leftSideBandwidth > tunerUsableHalfBandwidth) + { + //Ensure this first right-partition channel fits within the half bandwidth + if(tunerChannel.getBandwidth() > tunerUsableHalfBandwidth) + { + return false; + } + + firstRightChannel = tunerChannel; + } + } + else + { + long rightSideBandwidth = tunerChannel.getMaxFrequency() - firstRightChannel.getMinFrequency(); + + //If the right side partition's bandwidth exceeds the half bandwidth, we're done + if(rightSideBandwidth > tunerUsableHalfBandwidth) + { + return false; + } + } + } + + //All channels can be partitioned into notional left/right partition sets - we're good + return true; + } + } + return false; } - @Override - public List getChannels() + /** + * Calculates the optimal center tune frequency for the set of currently sourced tuner channels. Note: the returned + * frequency will only be different from the current center frequency argument if a change is required in order + * to fit all of the sourced tuner channels, or to better optimize the channels for alignment with the current + * polyphase channelizer channel partitioning. + * + * @param channels that are currently sourced by this source manager + * @param currentCenterFrequency of the tuner. + * @return optimal center tuned frequency for the set of currently sourced tuner channels. + * @throws IllegalArgumentException if a center frequency cannot be determined for the set of tuner channels + */ + private long getCenterFrequency(SortedSet channels, long currentCenterFrequency) + throws IllegalArgumentException { - return null; + if(channels.isEmpty()) + { + return currentCenterFrequency; + } + + long bestIntegralFrequency = getIntegralFrequency(channels); + + //See if we can reuse the current frequency + if(isIntegralSpacing(currentCenterFrequency, bestIntegralFrequency) && + isValidCenterFrequency(channels, currentCenterFrequency)) + { + return currentCenterFrequency; + } + + int integralHalfChannelCount = (int)(mTunerController.getUsableHalfBandwidth() / + mPolyphaseChannelManager.getChannelBandwidth()) + 1; + + int integralHalfBandwidth = (int)(integralHalfChannelCount * mPolyphaseChannelManager.getChannelBandwidth()); + + long minimumFrequency = channels.last().getFrequency() - integralHalfBandwidth; + long testFrequency = channels.first().getFrequency() + integralHalfBandwidth; + + if(isValidCenterFrequency(channels,testFrequency)) + { + return testFrequency; + } + + while(testFrequency >= minimumFrequency) + { + testFrequency -= (long)mPolyphaseChannelManager.getChannelBandwidth(); + + if(isValidCenterFrequency(channels, testFrequency)) + { + return testFrequency; + } + } + + //If we can't find an integral center frequency, choose a frequency that will fit the channels + minimumFrequency = channels.last().getFrequency() - mTunerController.getUsableHalfBandwidth(); + testFrequency = channels.first().getMinFrequency() + mTunerController.getUsableHalfBandwidth(); + + if(isValidCenterFrequency(channels,testFrequency)) + { + return testFrequency; + } + + while(testFrequency >= minimumFrequency) + { + testFrequency--; + + if(isValidCenterFrequency(channels, testFrequency)) + { + return testFrequency; + } + } + + throw new IllegalArgumentException("Can't calculate valid center frequency for the channel set"); } + /** + * Indicates if the candidate center frequency is valid to ensure that all of the channels fit within the tuner's + * bandwidth and avoid any center DC spike blocked frequency region + * + * @param tunerChannels to test for the candidate center frequency + * @param centerFrequency to test + * @return true if the candidate center frequency is valid for the set of channels + */ + private boolean isValidCenterFrequency(SortedSet tunerChannels, long centerFrequency) + { + for(TunerChannel tunerChannel: tunerChannels) + { + if(!isValidCenterFrequency(tunerChannel, centerFrequency)) + { + return false; + } + } + + return true; + } + + /** + * Indicates if the channel will fit within the tuner's bandwidth if the tuner were using the candidate center + * frequency and that the channel does not overlap an unusable center region that would be centered about the + * candidate center frequency + * + * @param tunerChannel to test for fit + * @param centerFrequency to test against + * @return true if the channel will fit when the tuner uses the specified center frequency + */ + private boolean isValidCenterFrequency(TunerChannel tunerChannel, long centerFrequency) + { + boolean fits = (tunerChannel.getMinFrequency() >= centerFrequency - mTunerController.getUsableHalfBandwidth()) && + (tunerChannel.getMaxFrequency() <= centerFrequency + mTunerController.getUsableHalfBandwidth()); + + if(fits && mTunerController.hasMiddleUnusableBandwidth()) + { + fits = !tunerChannel.overlaps(centerFrequency - mTunerController.getMiddleUnusableHalfBandwidth(), + centerFrequency + mTunerController.getMiddleUnusableHalfBandwidth()); + } + + return fits; + } + + /** + * Indicates if the two frequencies have a spacing that is an integral multiple of the channel bandwidth. + */ + private boolean isIntegralSpacing(long frequencyA, long frequencyB) + { + double delta = Math.abs(frequencyA - frequencyB); + return delta % mPolyphaseChannelManager.getChannelBandwidth() <= 1.0; + } + + /** + * Identifies the tuner channel frequency that has the best fit as an integral multiple of all of the other tuner + * channels. Using a polyphase channelizer to split each of the channels at a spacing of the channel sample rate, + * best fit is defined as the lowest absolute miss distance for each channel from the integral centers + * for each of the channels in the polyphase channelizer. + * + * @param channels containing a subset of channels that are spaced at integral multiples of the channel sample rate + * @return best channel containing the highest subset of matching, integrally spaced channels in the set, or null + * if there are no channels that are integrally spaced. + */ + private long getIntegralFrequency(SortedSet channels) + { + if(channels.isEmpty()) + { + throw new IllegalArgumentException("Channels cannot be empty"); + } + + long bestIntegralFrequency = channels.first().getFrequency(); + + if(channels.size() > 1) + { + double bestScore = Double.MAX_VALUE; + + for(TunerChannel firstChannel: channels) + { + double score = 0.0; + + for(TunerChannel secondChannel: channels) + { + if(firstChannel != secondChannel) + { + double delta = Math.abs(firstChannel.getFrequency() - secondChannel.getFrequency()) % + mPolyphaseChannelManager.getChannelBandwidth(); + + score += delta; + } + } + + if(score < bestScore) + { + bestScore = score; + bestIntegralFrequency = firstChannel.getFrequency(); + } + } + } + + return bestIntegralFrequency; + } + + /** + * List of tuner channels currently being sourced by this source manager + */ + @Override + public SortedSet getTunerChannels() + { + return mPolyphaseChannelManager.getTunerChannels(); + } + + /** + * Count of tuner channels currently being sourced by this source manager + */ + @Override + public int getTunerChannelCount() + { + return mPolyphaseChannelManager.getTunerChannelCount(); + } + + /** + * Allocates a tuner channel source for the tuner channel. + * + * @param tunerChannel for requested source + * @return allocated DDC tuner channel source, or null if the channel cannot be provided by this source manager + */ @Override public TunerChannelSource getSource(TunerChannel tunerChannel) { + if(isTunable(tunerChannel)) + { + //Get a new set of currently tuned channels + SortedSet tunerChannels = getTunerChannels(); + + //Add the requested channel to the list + tunerChannels.add(tunerChannel); + + if(canTune(tunerChannels)) + { + long currentCenterFrequency = mTunerController.getFrequency(); + long updatedCenterFrequency = 0; + + //Attempt to adjust the center frequency before we allocate the channel + try + { + updatedCenterFrequency = getCenterFrequency(tunerChannels, currentCenterFrequency); + + if(updatedCenterFrequency != currentCenterFrequency && updatedCenterFrequency != 0) + { + mTunerController.setFrequency(updatedCenterFrequency); + } + + //If we're successful to here, allocate the channel + return mPolyphaseChannelManager.getChannel(tunerChannel); + } + catch(SourceException se) + { + //Tuner controller threw an error trying to tune to the updated frequency + mLog.error("Error while updating tuner controller with new center frequency [" + + updatedCenterFrequency + "] - unable to allocate new tuner channel", se); + } + catch(IllegalArgumentException iae) + { + //Center frequency calculation failed + mLog.error("Couldn't calculate new tuner center frequency for new tuner channel [" + + tunerChannel + "] unable to allocate new tuner channel", iae); + } + } + } + return null; } + /** + * Processes source events received from the tuner by simply passing them on to the embedded polyphase channel + * manager + */ @Override - public void releaseSource(TunerChannelSource source) + public void process(SourceEvent sourceEvent) { - - } - - @Override - public void addSourceEventListener(Listener sourceEventListener) - { - - } - - @Override - public void removeSourceEventListener(Listener sourceEventListener) - { - - } - - @Override - public void receive(SourceEvent sourceEvent) - { - + switch(sourceEvent.getEvent()) + { + case NOTIFICATION_CHANNEL_COUNT_CHANGE: + //Rebroadcast this event to any registered listeners (ie tuner and tuner controller) + broadcast(sourceEvent); + break; + case NOTIFICATION_FREQUENCY_CHANGE: + mPolyphaseChannelManager.process(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mPolyphaseChannelManager.process(sourceEvent); + break; + default: + mLog.info("Received an unrecognized source event: " + sourceEvent); + break; + } } }