diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 967a782f9..000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml deleted file mode 100644 index 2bdceef59..000000000 --- a/.idea/libraries/imports.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index df60b6777..b67e72174 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,7 @@ - + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5e8edec26..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index fef64bf15..35eb1ddfb 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,16 +1,6 @@ - - - - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6dc7536db..0c75dee8d 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,7 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-jre' implementation 'com.jidesoft:jide-oss:3.6.18' implementation 'com.miglayout:miglayout-swing:5.2' + implementation 'com.mpatric:mp3agic:0.9.1' implementation 'eu.hansolo:charts:1.0.5' implementation 'io.github.dsheirer:radio-reference-api:15.1.2' implementation 'javax.usb:usb-api:1.0.2' @@ -71,6 +72,7 @@ dependencies { implementation 'org.apache.commons:commons-io:1.3.2' implementation 'org.apache.mina:mina-core:2.0.19' implementation 'org.apache.mina:mina-http:2.0.19' + implementation 'org.controlsfx:controlsfx:11.0.1' implementation 'org.slf4j:slf4j-api:1.7.25' implementation 'org.usb4java:libusb4java:1.3.0' implementation 'org.usb4java:usb4java:1.3.0' diff --git a/copyright.template b/copyright.template new file mode 100644 index 000000000..90d51310f --- /dev/null +++ b/copyright.template @@ -0,0 +1,16 @@ +****************************************************************************** +Copyright (C) 2014-${today.year} 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 +***************************************************************************** \ No newline at end of file diff --git a/sdr-trunk.ipr b/sdr-trunk.ipr index 9ff6b03bd..50ef3e983 100644 --- a/sdr-trunk.ipr +++ b/sdr-trunk.ipr @@ -30,6 +30,7 @@ + - diff --git a/src/main/java/io/github/dsheirer/alias/AliasList.java b/src/main/java/io/github/dsheirer/alias/AliasList.java index cbf3eec6d..b1793997b 100644 --- a/src/main/java/io/github/dsheirer/alias/AliasList.java +++ b/src/main/java/io/github/dsheirer/alias/AliasList.java @@ -538,9 +538,12 @@ public void add(Talkgroup talkgroup, Alias alias) { Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue()); - mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() + - "] has the same talkgroup value as alias [" + existing.getName() + - "] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]"); + if(!existing.equals(alias)) + { + mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() + + "] has the same talkgroup value as alias [" + existing.getName() + + "] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]"); + } } mTalkgroupAliasMap.put(talkgroup.getValue(), alias); @@ -551,7 +554,7 @@ public void add(TalkgroupRange talkgroupRange, Alias alias) //Log warning if the new talkgroup range overlaps with any existing ranges for(Map.Entry entry: mTalkgroupRangeAliasMap.entrySet()) { - if(talkgroupRange.overlaps(entry.getKey())) + if(talkgroupRange.overlaps(entry.getKey()) && !entry.getValue().equals(alias)) { mLog.warn("Alias [" + alias.getName() + "] with talkgroup range [" + talkgroupRange.toString() + "] overlaps with alias [" + entry.getValue().getName() + @@ -621,9 +624,12 @@ public void add(Radio radio, Alias alias) { Alias existing = mRadioAliasMap.get(radio.getValue()); - mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() + - "] has the same value as alias [" + existing.getName() + - "] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]"); + if(!existing.equals(alias)) + { + mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() + + "] has the same value as alias [" + existing.getName() + + "] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]"); + } } mRadioAliasMap.put(radio.getValue(), alias); @@ -634,7 +640,7 @@ public void add(RadioRange radioRange, Alias alias) //Log warning if the new range overlaps with any existing ranges for(Map.Entry entry: mRadioRangeAliasMap.entrySet()) { - if(radioRange.overlaps(entry.getKey())) + if(radioRange.overlaps(entry.getKey()) && !entry.getValue().equals(alias)) { mLog.warn("Alias [" + alias.getName() + "] with radio ID range [" + radioRange.toString() + "] overlaps with alias [" + entry.getValue().getName() + diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java index b243dc70f..f3ce2ba4e 100644 --- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java @@ -20,92 +20,168 @@ package io.github.dsheirer.audio; -import io.github.dsheirer.audio.squelch.ISquelchStateListener; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.identifier.IdentifierUpdateListener; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.module.Module; +import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Base audio module implementation. */ -public abstract class AbstractAudioModule extends Module implements IAudioPacketProvider, IdentifierUpdateListener, - ISquelchStateListener +public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener { - //Static unique audio channel identifier. - private static int AUDIO_CHANNEL_ID_GENERATOR = 1; + private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class); + private static final int MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute - private int mAudioChannelId = AUDIO_CHANNEL_ID_GENERATOR++; - private Listener mAudioPacketListener; + private Listener mAudioSegmentListener; private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection(); + private Broadcaster mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>(); + private AliasList mAliasList; + private AudioSegment mAudioSegment; + private int mAudioSampleCount = 0; + private boolean mRecordAudioOverride; /** * Constructs an abstract audio module */ - public AbstractAudioModule() + public AbstractAudioModule(AliasList aliasList) { + mAliasList = aliasList; + mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection); } + protected abstract int getTimeslot(); + /** - * Unique channel identifier for use in tagging audio packets with an audio channel ID so that they can - * be identified within a combined audio packet stream + * Closes the current audio segment */ - public int getAudioChannelId() + protected void closeAudioSegment() { - return mAudioChannelId; + synchronized(this) + { + if(mAudioSegment != null) + { + mAudioSegment.completeProperty().set(true); + mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment); + mAudioSegment = null; + } + } } - /** - * Receive updated identifiers from decoder state(s). - */ @Override - public Listener getIdentifierUpdateListener() + public void stop() { - return mIdentifierCollection; + closeAudioSegment(); } /** - * Identifier collection containing the current set of identifiers received from the decoder state(s). + * Gets the current audio segment, or creates a new audio segment as necessary and broadcasts it to any registered + * listener(s). */ - public MutableIdentifierCollection getIdentifierCollection() + protected AudioSegment getAudioSegment() { - return mIdentifierCollection; + synchronized(this) + { + if(mAudioSegment == null) + { + mAudioSegment = new AudioSegment(mAliasList, getTimeslot()); + mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers()); + mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment); + + if(mRecordAudioOverride) + { + mAudioSegment.recordAudioProperty().set(true); + } + + if(mAudioSegmentListener != null) + { + mAudioSegment.incrementConsumerCount(); + mAudioSegmentListener.receive(mAudioSegment); + } + + mAudioSampleCount = 0; + } + + return mAudioSegment; + } + } + + protected void addAudio(float[] audioBuffer) + { + AudioSegment audioSegment = getAudioSegment(); + + //If the current segment exceeds the max samples length, close it so that a new segment gets generated + //and then link the segments together + if(mAudioSampleCount >= MAX_SEGMENT_AUDIO_SAMPLE_LENGTH) + { + AudioSegment previous = getAudioSegment(); + closeAudioSegment(); + audioSegment = getAudioSegment(); + audioSegment.linkTo(previous); + } + + audioSegment.addAudio(audioBuffer); + mAudioSampleCount += audioBuffer.length; } /** - * Registers an audio packet listener to receive the output from this audio module. + * Sets all audio segments as recordable when the argument is true. Otherwise, defers to the aliased identifiers + * from the identifier collection to determine whether to record the audio or not. + * @param recordAudio set to true to mark all audio as recordable. */ - @Override - public void setAudioPacketListener(Listener listener) + public void setRecordAudio(boolean recordAudio) { - mAudioPacketListener = listener; + mRecordAudioOverride = recordAudio; + + if(mRecordAudioOverride) + { + synchronized(this) + { + if(mAudioSegment != null) + { + mAudioSegment.recordAudioProperty().set(true); + } + } + } } /** - * Unregisters the audio packet listener from receiving audio packets from this module. + * Receive updated identifiers from decoder state(s). */ @Override - public void removeAudioPacketListener() + public Listener getIdentifierUpdateListener() { - mAudioPacketListener = null; + return mIdentifierUpdateNotificationBroadcaster; } /** - * Registered listener for receiving audio packets produced by this module + * Identifier collection containing the current set of identifiers received from the decoder state(s). */ - public Listener getAudioPacketListener() + public MutableIdentifierCollection getIdentifierCollection() { - return mAudioPacketListener; + return mIdentifierCollection; } /** - * Indicates if there is a listener registered to receive audio packets from this audio module. + * Registers an audio segment listener to receive the output from this audio module. */ - public boolean hasAudioPacketListener() + @Override + public void setAudioSegmentListener(Listener listener) { - return mAudioPacketListener != null; + mAudioSegmentListener = listener; } + /** + * Unregisters the audio segment listener from receiving audio segments from this module. + */ + @Override + public void removeAudioSegmentListener() + { + mAudioSegmentListener = null; + } } diff --git a/src/main/java/io/github/dsheirer/audio/AudioMetadataProcessor.java b/src/main/java/io/github/dsheirer/audio/AudioMetadataProcessor.java deleted file mode 100644 index 9559ffa63..000000000 --- a/src/main/java/io/github/dsheirer/audio/AudioMetadataProcessor.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 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.audio; - -import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; - -/** - * Processes audio packets to assign attributes to each packet based on alias list settings. Updates the - * monitoring, recording and streaming attributes. - */ -public class AudioMetadataProcessor -{ - private static final Logger mLog = LoggerFactory.getLogger(AudioMetadataProcessor.class); - private AliasModel mAliasModel; - private Map mAliasListMap = new HashMap<>(); - private Set mAliasListRetrievedSet = new TreeSet<>(); - - public AudioMetadataProcessor(AliasModel aliasModel) - { - mAliasModel = aliasModel; - } - - public void process(ReusableAudioPacket audioPacket) - { - if(audioPacket.hasIdentifierCollection() && audioPacket.getIdentifierCollection().hasAliasListConfiguration()) - { - AliasList aliasList = null; - - if(mAliasListRetrievedSet.contains(audioPacket.getAudioChannelId())) - { - aliasList = mAliasListMap.get(audioPacket.getAudioChannelId()); - } - else - { - aliasList = mAliasModel.getAliasList(audioPacket.getIdentifierCollection().getAliasListConfiguration()); - - if(aliasList != null) - { - mAliasListMap.put(audioPacket.getAudioChannelId(), aliasList); - mAliasListRetrievedSet.add(audioPacket.getAudioChannelId()); - } - } - - if(aliasList != null) - { - audioPacket.addBroadcastChannels(aliasList.getBroadcastChannels(audioPacket.getIdentifierCollection())); - audioPacket.setMonitoringPriority(aliasList.getAudioPlaybackPriority(audioPacket.getIdentifierCollection())); - - //If the audio packet is already marked recordable, leave it alone, otherwise attempt to determine - //if we should record the audio packet from the aliased identifiers - if(!audioPacket.isRecordable()) - { - audioPacket.setRecordable(aliasList.isRecordable(audioPacket.getIdentifierCollection())); - } - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java index d8be85dcb..5d3362137 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java @@ -21,6 +21,8 @@ */ package io.github.dsheirer.audio; +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.audio.squelch.ISquelchStateListener; import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.audio.squelch.SquelchStateEvent; import io.github.dsheirer.dsp.filter.design.FilterDesignException; @@ -29,23 +31,19 @@ import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner; import io.github.dsheirer.sample.Listener; 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.ReusableFloatBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * 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 segments for broadcast to registered listeners. + * Includes audio packet metadata in constructed audio segments. * * 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 AbstractAudioModule implements IReusableBufferListener, Listener +public class AudioModule extends AbstractAudioModule implements ISquelchStateListener, IReusableBufferListener, + Listener { - private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class); private static float[] sHighPassFilterCoefficients; @@ -75,34 +73,29 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL } } - private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule"); private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients); private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private SquelchState mSquelchState = SquelchState.SQUELCH; - private boolean mRecordAudioOverride; /** * Creates an Audio Module. */ - public AudioModule() + public AudioModule(AliasList aliasList) { + super(aliasList); } @Override - public void dispose() + protected int getTimeslot() { - removeAudioPacketListener(); - mSquelchStateListener = null; + return 0; } - /** - * Sets all audio packets as recordable when the argument is true. Otherwise, defers to the aliased identifiers - * from the identifier collection to determine whether to record the audio or not. - * @param recordAudio set to true to mark all audio as recordable. - */ - public void setRecordAudio(boolean recordAudio) + @Override + public void dispose() { - mRecordAudioOverride = recordAudio; + removeAudioSegmentListener(); + mSquelchStateListener = null; } @Override @@ -114,30 +107,8 @@ public void reset() @Override public void start() { - /* No start operations provided */ - } - - @Override - public void stop() - { - /* Issue an end-audio packet in case a recorder is still rolling */ - if(hasAudioPacketListener()) - { - ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer(); - endAudioPacket.resetAttributes(); - endAudioPacket.setAudioChannelId(getAudioChannelId()); - endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - - if(mRecordAudioOverride) - { - endAudioPacket.setRecordable(true); - } - endAudioPacket.incrementUserCount(); - getAudioPacketListener().receive(endAudioPacket); - } } - @Override public Listener getSquelchStateListener() { @@ -147,23 +118,11 @@ public Listener getSquelchStateListener() @Override public void receive(ReusableFloatBuffer reusableFloatBuffer) { - if(hasAudioPacketListener() && mSquelchState == SquelchState.UNSQUELCH) + if(mSquelchState == SquelchState.UNSQUELCH) { - ReusableFloatBuffer highPassFiltered = mHighPassFilter.filter(reusableFloatBuffer); - - ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount()); - audioPacket.resetAttributes(); - audioPacket.setAudioChannelId(getAudioChannelId()); - audioPacket.loadAudioFrom(highPassFiltered); - if(mRecordAudioOverride) - { - audioPacket.setRecordable(true); - } - audioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - - getAudioPacketListener().receive(audioPacket); - - highPassFiltered.decrementUserCount(); + ReusableFloatBuffer buffer = mHighPassFilter.filter(reusableFloatBuffer); + addAudio(buffer.getSamples()); + buffer.decrementUserCount(); } else { @@ -186,16 +145,12 @@ public class SquelchStateListener implements Listener @Override public void receive(SquelchStateEvent event) { - if(event.getSquelchState() == SquelchState.SQUELCH && hasAudioPacketListener()) + mSquelchState = event.getSquelchState(); + + if(mSquelchState == SquelchState.SQUELCH) { - ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer(); - endAudioPacket.resetAttributes(); - endAudioPacket.setAudioChannelId(getAudioChannelId()); - endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - getAudioPacketListener().receive(endAudioPacket); + closeAudioSegment(); } - - mSquelchState = event.getSquelchState(); } } } diff --git a/src/main/java/io/github/dsheirer/audio/AudioPacketManager.java b/src/main/java/io/github/dsheirer/audio/AudioPacketManager.java deleted file mode 100644 index e6c0ae360..000000000 --- a/src/main/java/io/github/dsheirer/audio/AudioPacketManager.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * ****************************************************************************** - * sdrtrunk - * 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.audio; - -import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.dsp.filter.channelizer.ContinuousReusableBufferProcessor; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; -import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -/** - * Processes audio packets from all decoder channels, assigns alias attributes and distributes packets - * for playback, streaming and recording. - */ -public class AudioPacketManager implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(AudioPacketManager.class); - private AudioMetadataProcessor mAudioMetadataProcessor; - private ReusableBufferBroadcaster mBroadcaster = new ReusableBufferBroadcaster<>(); - private ContinuousReusableBufferProcessor mBufferProcessor; - - public AudioPacketManager(AliasModel aliasModel) - { - mAudioMetadataProcessor = new AudioMetadataProcessor(aliasModel); - mBufferProcessor = new ContinuousReusableBufferProcessor<>(10000, 500); - mBufferProcessor.setListener(new Listener>() - { - @Override - public void receive(List reusableAudioPackets) - { - for(ReusableAudioPacket audioPacket: reusableAudioPackets) - { - mAudioMetadataProcessor.process(audioPacket); - mBroadcaster.broadcast(audioPacket); - } - } - }); - } - - public void start() - { - mLog.info("Audio packet manager started"); - mBufferProcessor.start(); - } - - public void stop() - { - mLog.info("Audio packet manager stopped"); - mBufferProcessor.stop(); - } - - /** - * Primary processing method for receiving audio packets from all processing channels. - */ - @Override - public void receive(ReusableAudioPacket audioPacket) - { - mBufferProcessor.receive(audioPacket); - } - - /** - * Adds the listener to receive enriched audio packets from this manager - */ - public void addListener(Listener listener) - { - mBroadcaster.addListener(listener); - } - - /** - * Removes the listener from receiving enriched audio packets from this manager - */ - public void removeListener(Listener listener) - { - mBroadcaster.removeListener(listener); - } -} diff --git a/src/main/java/io/github/dsheirer/audio/AudioSegment.java b/src/main/java/io/github/dsheirer/audio/AudioSegment.java new file mode 100644 index 000000000..c6c38e9d6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/AudioSegment.java @@ -0,0 +1,401 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2020 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.audio; + +import io.github.dsheirer.alias.Alias; +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; +import io.github.dsheirer.alias.id.priority.Priority; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.IdentifierUpdateNotification; +import io.github.dsheirer.identifier.MutableIdentifierCollection; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Audio segment containing all related metadata and a dynamic collection of audio packets. An audio segment can be + * a discrete (ie start/stop) audio event, or it may be a time-constrained portion of an ongoing continuous audio + * broadcast. Since the audio segment is held in memory until all consumers have finished processing the segment, + * producers should constrain the duration of each audio segment to a reasonable duration. + * + * Producers can link time-constrained audio segments from a continous broadcast (e.g. FM radio station) so that + * consumers can identify audio segments that belong to a continous stream. This linkage presents a memory leak + * potential. Therefore, the audio segment uses the consumer counter to trigger an unlinking via the dispose() mehtod. + * Accurate accounting of consumer count via the increment/decrementConsumerCount() methods is essential for good + * memory management. + * + * Producers will add audio buffers and update identifiers throughout the life-cycle of an audio segment. The producer + * will signal the completion of an audio segment by setting the complete property to true. This allows consumers the + * option to process the audio buffers throughout the life-cycle of the segment, or to process all of the buffers once + * the segment is complete. + */ +public class AudioSegment implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class); + private BooleanProperty mComplete = new SimpleBooleanProperty(false); + private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false); + private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY); + private ObservableSet mBroadcastChannels = FXCollections.observableSet(new HashSet<>()); + private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection(); + private Broadcaster mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>(); + private List mAudioBuffers = new CopyOnWriteArrayList(); + private AtomicInteger mConsumerCount = new AtomicInteger(); + private AliasList mAliasList; + private long mStartTimestamp = System.currentTimeMillis(); + private boolean mDisposing = false; + private AudioSegment mLinkedAudioSegment; + + /** + * Constructs an instance + * + * @param aliasList for accessing aliases associated with identifiers for this audio segment. + */ + public AudioSegment(AliasList aliasList, int timeslot) + { + mAliasList = aliasList; + mIdentifierCollection.setTimeslot(timeslot); + } + + /** + * Creation timestamp for this audio segment + * @return long milliseconds since epoch + */ + public long getStartTimestamp() + { + return mStartTimestamp; + } + + /** + * The complete property is used by the audio segment producer to signal that the segment is complete and no + * additional audio or identifiers will be added to the segment. + * + * @return complete property + */ + public BooleanProperty completeProperty() + { + return mComplete; + } + + /** + * An observable list of broadcast channels for this audio segment. Broadcast channels are added to this segment + * across the life-cycle of the segment. As each new alias identifier is added to the segment, any broadcast + * channels assigned to the alias are added to this list. The audio segment producer can also add broadcast + * channels to this list. + * + * @return observable set of broadcast channels + */ + public ObservableSet broadcastChannelsProperty() + { + return mBroadcastChannels; + } + + /** + * Set of broadcast channels from identifier associated aliases for this segment. + */ + public Set getBroadcastChannels() + { + return Collections.unmodifiableSet(mBroadcastChannels); + } + + /** + * Indicates if this segment has audio streaming broadcast channels specified. + */ + public boolean hasBroadcastChannels() + { + return !mBroadcastChannels.isEmpty(); + } + + /** + * Property to signal that this audio segment should be recorded. This property can either be set by the producer + * of the audio segment, or it can be flipped to true by any aliases that are added to this segment that require + * audio associated with the identifier to be recorded. + * + * @return + */ + public BooleanProperty recordAudioProperty() + { + return mRecordAudio; + } + + /** + * Audio playback/monitor priority specified by identifier associated aliases for this segment. + */ + public IntegerProperty monitorPriorityProperty() + { + return mMonitorPriority; + } + + /** + * Indicates if at least one of the identifier associated aliases for this segments specifies Do Not Monitor. + */ + public boolean isDoNotMonitor() + { + return mMonitorPriority.get() == Priority.DO_NOT_MONITOR; + } + + /** + * Alias list for this audio segment + */ + public AliasList getAliasList() + { + return mAliasList; + } + + /** + * Indicates if this audio segment is linked to a preceding audio segment. + */ + public boolean isLinked() + { + return mLinkedAudioSegment != null; + } + + /** + * Indicates if this audio segment is linked to the argument audio segment + * @param audioSegment to check for linkage + * @return true if this segment is linked to the argument + */ + public boolean isLinkedTo(AudioSegment audioSegment) + { + return isLinked() && audioSegment != null && mLinkedAudioSegment.equals(audioSegment); + } + + /** + * Optional linked audio segment is the audio segment that precedes this audio segment in a continuous audio + * broadcast. + * + * @return linked audio segment or null. + */ + public AudioSegment getLinkedAudioSegment() + { + return mLinkedAudioSegment; + } + + /** + * Links this audio segment to a preceeding/previous audio segment + * @param previousAudioSegment to set as the predecessor + */ + public void linkTo(AudioSegment previousAudioSegment) + { + mLinkedAudioSegment = previousAudioSegment; + } + + /** + * Identifier collection for this audio segment + */ + public IdentifierCollection getIdentifierCollection() + { + return mIdentifierCollection; + } + + /** + * Adds the collection of identifiers to this segment's identifier collection + * @param identifiers to pre-load into this audio segment + */ + void addIdentifiers(Collection identifiers) + { + for(Identifier identifier: identifiers) + { + mIdentifierCollection.update(identifier); + } + } + + /** + * Unmodifiable copy of the list of audio buffers for this segment. + * + * @return list of audio buffers + */ + public List getAudioBuffers() + { + return Collections.unmodifiableList(mAudioBuffers); + } + + /** + * Count of audio buffers contained in this segment. + * + * Note: audio buffers can be added to an audio segment throughout the segment's life-cycle by the audio producer. + */ + public int getAudioBufferCount() + { + return mAudioBuffers.size(); + } + + /** + * Gets the audio buffer at the specified index + * @param index of the buffer to fetch + * @return audio buffer + * @throws IllegalArgumentException if requested index is not valid + */ + public float[] getAudioBuffer(int index) + { + if(0 <= index && index < getAudioBufferCount()) + { + return mAudioBuffers.get(index); + } + else + { + throw new IllegalArgumentException("Requested audio buffer at index [" + index + "] does not exist"); + } + } + + /** + * Indicates if this audio segment has one or more audio buffers + */ + public boolean hasAudio() + { + return !mAudioBuffers.isEmpty(); + } + + /** + * Removes all audio buffers and decrements the user count on each so that the audio buffer can be reclaimed. + */ + private void dispose() + { + mDisposing = true; + mAudioBuffers.clear(); + mIdentifierCollection.clear(); + mIdentifierUpdateNotificationBroadcaster.clear(); + mLinkedAudioSegment = null; + } + + /** + * Increments the consumer count to indicate that a consumer is currently processing this segment. When the + * consumer count returns to zero, this indicates that all consumers are finished with the audio segment and the + * resources can be reclaimed. + * + * Consumer count should only be increased by the producer of the audio segment, or if a consumer distributes the + * segment to additional consumers. + */ + public void incrementConsumerCount() + { + mConsumerCount.incrementAndGet(); + } + + /** + * Decrements the consumer count. Consumers of this audio segment should invoke this method to signal that they + * will no longer need this audio segment. When all consumers are finished with an audio segment, the audio + * segment resources will be reclaimed. + */ + public void decrementConsumerCount() + { + int count = mConsumerCount.decrementAndGet(); + + if(count <= 0) + { + dispose(); + } + } + + /** + * Adds an audio buffer to this segment. Note the producer of the audio buffer should increment the user count + * of the buffer prior to adding it to this segment. This segment will decrement the audio buffer user count once + * all consumers of this audio segment have de-registered via the decrementConsumerCount() method. + * + * @param audioBuffer to add to this segment + */ + public void addAudio(float[] audioBuffer) + { + if(audioBuffer == null) + { + throw new IllegalArgumentException("Can't add null audio buffer"); + } + + if(mDisposing) + { + throw new IllegalStateException("Can't add audio to an audio segment that is being disposed"); + } + + mAudioBuffers.add(audioBuffer); + } + + /** + * Adds a listener to receive identifier update notifications + */ + public void addIdentifierUpdateNotificationListener(Listener listener) + { + mIdentifierUpdateNotificationBroadcaster.addListener(listener); + } + + /** + * Removes the identifier update listener. + */ + public void removeIdentifierUpdateNotificationListener(Listener listener) + { + mIdentifierUpdateNotificationBroadcaster.removeListener(listener); + } + + /** + * Processes identifier update notifications and updates audio segment properties using the associated aliases + * from the alias list. + * + * @param identifierUpdateNotification containing an identifier. + */ + @Override + public void receive(IdentifierUpdateNotification identifierUpdateNotification) + { + IdentifierUpdateNotification.Operation operation = identifierUpdateNotification.getOperation(); + + //Only process ADD updates. Ignore REMOVE to preserve the full metadata set + if(operation == IdentifierUpdateNotification.Operation.ADD || + operation == IdentifierUpdateNotification.Operation.SILENT_ADD) + { + mIdentifierCollection.receive(identifierUpdateNotification); + + List aliases = mAliasList.getAliases(identifierUpdateNotification.getIdentifier()); + + for(Alias alias: aliases) + { + if(alias.isRecordable()) + { + mRecordAudio.set(true); + } + + //Add all broadcast channels for the alias ... let the set handle duplication. + for(BroadcastChannel broadcastChannel: alias.getBroadcastChannels()) + { + mBroadcastChannels.add(broadcastChannel); + } + + //Only assign a playback priority if it is lower priority than the current setting. + int playbackPriority = alias.getPlaybackPriority(); + + if(playbackPriority < mMonitorPriority.get()) + { + mMonitorPriority.set(playbackPriority); + } + } + + mIdentifierUpdateNotificationBroadcaster.broadcast(identifierUpdateNotification); + } + } +} diff --git a/src/main/java/io/github/dsheirer/audio/AudioSegmentBroadcaster.java b/src/main/java/io/github/dsheirer/audio/AudioSegmentBroadcaster.java new file mode 100644 index 000000000..43e65f343 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/AudioSegmentBroadcaster.java @@ -0,0 +1,52 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.audio; + +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.buffer.AbstractReusableBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AudioSegmentBroadcaster extends Broadcaster +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioSegmentBroadcaster.class); + + /** + * Increments the consumer count for the reusable complex buffer and then broadcasts the buffer to all registered + * listeners. + * + * The total consumer count is established and applied to the buffer prior to dispatching. If we were to simply + * increment the consumer count prior to sending to each consumer, there is a possibility that the consumer could + * immediately decrement the consumer count and prematurely signal that the buffer is ready for disposal before we + * send the buffer to all consumers. + */ + @Override + public void broadcast(T audioSegment) + { + for(Listener listener : mListeners) + { + audioSegment.incrementConsumerCount(); + listener.receive(audioSegment); + } + + //Decrement consumer counter for this broadcaster + audioSegment.decrementConsumerCount(); + } +} diff --git a/src/main/java/io/github/dsheirer/audio/AudioUtils.java b/src/main/java/io/github/dsheirer/audio/AudioUtils.java index 1eca23613..a42f67841 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioUtils.java +++ b/src/main/java/io/github/dsheirer/audio/AudioUtils.java @@ -16,7 +16,6 @@ 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; @@ -32,18 +31,17 @@ public class AudioUtils /** * Converts the audio packets into a byte array of 16-bit, little-endian audio samples */ - public static byte[] convertTo16BitSamples(List audioPackets) + public static byte[] convertTo16BitSamples(List buffers) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { - for(ReusableAudioPacket audioPacket: audioPackets) + for(float[] audioBuffer: buffers) { //Converting from 32-bit floats to signed 16-bit samples - ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples()); + ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioBuffer); stream.write(buffer.array()); - audioPacket.decrementUserCount(); } } catch(IOException e) @@ -53,4 +51,29 @@ public static byte[] convertTo16BitSamples(List audioPacket return stream.toByteArray(); } + + /** + * Converts the audio packets into a byte array of 16-bit, little-endian audio samples + */ + public static byte[] convert(List audioBuffers) + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + try + { + for(float[] audioBuffer: audioBuffers) + { + //Converting from 32-bit floats to signed 16-bit samples + ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioBuffer); + stream.write(buffer.array()); + } + } + catch(IOException e) + { + mLog.error("Error writing converted PCM bytes to output stream"); + } + + return stream.toByteArray(); + } + } diff --git a/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java b/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java deleted file mode 100644 index 76452704f..000000000 --- a/src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.dsheirer.audio; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; - -public interface IAudioPacketListener -{ - public Listener getAudioPacketListener(); -} diff --git a/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java b/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java deleted file mode 100644 index 7fc00a58c..000000000 --- a/src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -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 listener ); - public void removeAudioPacketListener(); -} diff --git a/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java b/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java new file mode 100644 index 000000000..8a25ffc93 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java @@ -0,0 +1,33 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.audio; + +import io.github.dsheirer.sample.Listener; + +/** + * Interface for audio segment listener + */ +public interface IAudioSegmentListener +{ + /** + * Get the audio segment listener + */ + Listener getAudioSegmentListener(); +} diff --git a/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java b/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java new file mode 100644 index 000000000..41a44ff44 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java @@ -0,0 +1,39 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.audio; + +import io.github.dsheirer.sample.Listener; + +/** + * Interface for audio segment provider + */ +public interface IAudioSegmentProvider +{ + /** + * Registers the audio segment listener + * @param listener of audio segments + */ + void setAudioSegmentListener(Listener listener); + + /** + * De-registers the audio segment listener + */ + void removeAudioSegmentListener(); +} diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioRecording.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioRecording.java index aa6720636..5b70c83a9 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioRecording.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioRecording.java @@ -18,22 +18,25 @@ ******************************************************************************/ package io.github.dsheirer.audio.broadcast; +import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.identifier.IdentifierCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.file.Path; +import java.util.Collection; import java.util.concurrent.atomic.AtomicInteger; public class AudioRecording implements Comparable { - private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class); + private final static Logger mLog = LoggerFactory.getLogger(AudioRecording.class); private Path mPath; private long mStartTime; private long mRecordingLength; private AtomicInteger mPendingReplayCount = new AtomicInteger(); private IdentifierCollection mIdentifierCollection; + private Collection mBroadcastChannels; /** * Audio recording that is ready to be streamed @@ -43,9 +46,11 @@ public class AudioRecording implements Comparable * @param start time of recording in milliseconds since epoch * @param recordingLength in milliseconds */ - public AudioRecording(Path path, IdentifierCollection identifierCollection, long start, long recordingLength) + public AudioRecording(Path path, Collection broadcastChannels, + IdentifierCollection identifierCollection, long start, long recordingLength) { mPath = path; + mBroadcastChannels = broadcastChannels; mIdentifierCollection = identifierCollection; mStartTime = start; mRecordingLength = recordingLength; @@ -59,6 +64,14 @@ public Path getPath() return mPath; } + /** + * Collection of broadcast channels that this recording should be streamed to + */ + public Collection getBroadcastChannels() + { + return mBroadcastChannels; + } + /** * Optional audio metadata/identifiers for the recording. */ diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java new file mode 100644 index 000000000..be4d93472 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java @@ -0,0 +1,206 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.audio.broadcast; + +import io.github.dsheirer.audio.AudioSegment; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.record.AudioSegmentRecorder; +import io.github.dsheirer.record.RecordFormat; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.util.ThreadPool; +import io.github.dsheirer.util.TimeStamp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on + * disk and enqueues the temporary recording for streaming. + */ +public class AudioStreamingManager implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingManager.class); + private LinkedTransferQueue mNewAudioSegments = new LinkedTransferQueue<>(); + private List mAudioSegments = new ArrayList<>(); + private Listener mAudioRecordingListener; + private BroadcastFormat mBroadcastFormat; + private UserPreferences mUserPreferences; + private ScheduledFuture mAudioSegmentProcessorFuture; + private int mNextRecordingNumber = 1; + + /** + * Constructs an instance + * @param listener to receive completed audio recordings + * @param broadcastFormat for temporary recordings + * @param userPreferences to manage recording directories + */ + public AudioStreamingManager(Listener listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences) + { + mAudioRecordingListener = listener; + mBroadcastFormat = broadcastFormat; + mUserPreferences = userPreferences; + } + + /** + * Primary receive method + */ + @Override + public void receive(AudioSegment audioSegment) + { + mNewAudioSegments.add(audioSegment); + } + + /** + * Starts the scheduled audio segment processor + */ + public void start() + { + if(mAudioSegmentProcessorFuture == null) + { + mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(), + 0, 250, TimeUnit.MILLISECONDS); + } + } + + /** + * Stops the scheduled audio segment processor + */ + public void stop() + { + if(mAudioSegmentProcessorFuture != null) + { + mAudioSegmentProcessorFuture.cancel(true); + mAudioSegmentProcessorFuture = null; + } + + for(AudioSegment audioSegment: mNewAudioSegments) + { + audioSegment.decrementConsumerCount(); + } + + mNewAudioSegments.clear(); + + for(AudioSegment audioSegment: mAudioSegments) + { + audioSegment.decrementConsumerCount(); + } + + mAudioSegments.clear(); + } + + /** + * Main processing method to process audio segments + */ + private void processAudioSegments() + { + mNewAudioSegments.drainTo(mAudioSegments); + + Iterator it = mAudioSegments.iterator(); + AudioSegment audioSegment; + while(it.hasNext()) + { + audioSegment = it.next(); + + if(audioSegment.completeProperty().get()) + { + it.remove(); + + if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels()) + { + Path path = getTemporaryRecordingPath(); + long length = 0; + + for(float[] audioBuffer: audioSegment.getAudioBuffers()) + { + length += audioBuffer.length; + } + + length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond. + + try + { + AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3); + AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(), + audioSegment.getIdentifierCollection(), audioSegment.getStartTimestamp(), length); + mAudioRecordingListener.receive(audioRecording); + } + catch(IOException ioe) + { + mLog.error("Error recording temporary stream MP3"); + } + } + + audioSegment.decrementConsumerCount(); + } + } + } + + /** + * Creates a temporary streaming recording file path + */ + private Path getTemporaryRecordingPath() + { + StringBuilder sb = new StringBuilder(); + sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX); + + //Check for integer overflow and readjust negative value to 0 + if(mNextRecordingNumber < 0) + { + mNextRecordingNumber = 1; + } + + int recordingNumber = mNextRecordingNumber++; + + sb.append(recordingNumber).append("_"); + sb.append(TimeStamp.getLongTimeStamp("_")); + sb.append(mBroadcastFormat.getFileExtension()); + + Path temporaryRecordingPath = mUserPreferences.getDirectoryPreference().getDirectoryStreaming().resolve(sb.toString()); + + return temporaryRecordingPath; + } + + /** + * Scheduled runnable to process audio segments. + */ + public class AudioSegmentProcessor implements Runnable + { + @Override + public void run() + { + try + { + processAudioSegments(); + } + catch(Throwable t) + { + mLog.error("Error processing audio segments for streaming", t); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java index 69688d346..d41484820 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java @@ -44,13 +44,9 @@ import io.github.dsheirer.gui.editor.EmptyEditor; import io.github.dsheirer.icon.IconManager; import io.github.dsheirer.preference.UserPreferences; -import io.github.dsheirer.record.AudioRecorder; -import io.github.dsheirer.record.mp3.MP3Recorder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Path; - public class BroadcastFactory { private final static Logger mLog = LoggerFactory.getLogger(BroadcastFactory.class); @@ -186,21 +182,6 @@ public static Editor getEditor(UserPreferences userPrefe return editor; } - /** - * Creates an audio recorder for the specified broadcastAudio format using the specified path output file name - */ - public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat) - { - switch(broadcastFormat) - { - case MP3: - return new MP3Recorder(path); - default: - mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder"); - return null; - } - } - public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format) { switch(format) diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java index 70dc39839..4018032fe 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java @@ -21,7 +21,6 @@ */ package io.github.dsheirer.audio.broadcast; -import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.icon.IconManager; @@ -29,7 +28,6 @@ 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; @@ -51,7 +49,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class BroadcastModel extends AbstractTableModel implements Listener +public class BroadcastModel extends AbstractTableModel implements Listener { private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class); @@ -76,7 +74,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener mBroadcastConfigurationMap = new HashMap<>(); private Map mBroadcasterMap = new HashMap<>(); private IconManager mIconManager; - private StreamManager mStreamManager; private AliasModel mAliasModel; private Broadcaster mBroadcastEventBroadcaster = new Broadcaster<>(); @@ -87,8 +84,6 @@ public BroadcastModel(AliasModel aliasModel, IconManager iconManager, UserPrefer { mAliasModel = aliasModel; mIconManager = iconManager; - mStreamManager = new StreamManager(new CompletedRecordingListener(), BroadcastFormat.MP3, userPreferences); - mStreamManager.start(); //Monitor to remove temporary recording files that have been streamed by all audio broadcasters ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS); @@ -281,22 +276,28 @@ public AudioBroadcaster getBroadcaster(String streamName) } @Override - public void receive(ReusableAudioPacket audioPacket) + public void receive(AudioRecording audioRecording) { - if(audioPacket.isStreamable()) + if(audioRecording != null && !audioRecording.getBroadcastChannels().isEmpty()) { - for(BroadcastChannel channel: audioPacket.getBroadcastChannels()) + for(BroadcastChannel broadcastChannel : audioRecording.getBroadcastChannels()) { - if(mBroadcasterMap.containsKey(channel.getChannelName())) + String channelName = broadcastChannel.getChannelName(); + + if(channelName != null) { - audioPacket.incrementUserCount(); - mStreamManager.receive(audioPacket); - return; + AudioBroadcaster audioBroadcaster = getBroadcaster(channelName); + + if(audioBroadcaster != null) + { + audioRecording.addPendingReplay(); + audioBroadcaster.receive(audioRecording); + } } } } - audioPacket.decrementUserCount(); + mRecordingQueue.add(audioRecording); } /** @@ -311,19 +312,9 @@ private void createBroadcaster(BroadcastConfiguration broadcastConfiguration) if(audioBroadcaster != null) { - audioBroadcaster.setListener(new Listener() - { - @Override - public void receive(BroadcastEvent broadcastEvent) - { - process(broadcastEvent); - } - }); - + audioBroadcaster.setListener(broadcastEvent -> process(broadcastEvent)); audioBroadcaster.start(); - mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster); - int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration()); if(index >= 0) @@ -710,43 +701,6 @@ public void run() } } - /** - * Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to - * the audio recording queue to be monitored for deletion. - */ - public class CompletedRecordingListener implements Listener - { - @Override - public void receive(AudioRecording audioRecording) - { - if(audioRecording != null && audioRecording.hasIdentifierCollection()) - { - AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection()); - - if(aliasList != null) - { - for(BroadcastChannel broadcastChannel : aliasList.getBroadcastChannels(audioRecording.getIdentifierCollection())) - { - String channelName = broadcastChannel.getChannelName(); - - if(channelName != null) - { - AudioBroadcaster audioBroadcaster = getBroadcaster(channelName); - - if(audioBroadcaster != null) - { - audioRecording.addPendingReplay(); - audioBroadcaster.receive(audioRecording); - } - } - } - } - } - - mRecordingQueue.add(audioRecording); - } - } - /** * Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters */ diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java b/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java deleted file mode 100644 index 39a27b9d0..000000000 --- a/src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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.audio.broadcast; - -import io.github.dsheirer.preference.UserPreferences; -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; -import org.slf4j.LoggerFactory; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -public class StreamManager implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class); - private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds - - private static AtomicInteger sNextRecordingNumber = new AtomicInteger(); - - private Listener mAudioRecordingListener; - private BroadcastFormat mBroadcastFormat; - private UserPreferences mUserPreferences; - private Map mStreamRecorders = new HashMap<>(); - private Runnable mRecorderMonitor; - private ScheduledFuture mRecorderMonitorFuture; - private AtomicBoolean mRunning = new AtomicBoolean(); - - /** - * Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio - * to desired output format and persists each stream to disc. Each recording is capped at a maximum length to - * ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings - * are closed in a timely fashion. - * - * Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion - * - * @param listener to receive completed audio recordings - * @param broadcastFormat for the stream - * @param userPreferences to obtain the temporary streaming recording directory - */ - public StreamManager(Listener listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences) - { - mAudioRecordingListener = listener; - mBroadcastFormat = broadcastFormat; - mUserPreferences = userPreferences; - } - - /** - * Starts the stream manager. Schedules a processor to monitor for inactive temporary stream recorders - * every 2 seconds. - */ - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - if(mRecorderMonitor == null) - { - mRecorderMonitor = new RecorderMonitor(); - } - - mRecorderMonitorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mRecorderMonitor, - 0,2, TimeUnit.SECONDS); - } - } - - /** - * Stops the stream manager. Stops the inactive temporary stream recorder monitoring thread. - */ - public void stop() - { - if(mRunning.compareAndSet(true, false)) - { - if(mRecorderMonitorFuture != null) - { - mRecorderMonitorFuture.cancel(true); - } - - synchronized(mStreamRecorders) - { - List streamKeys = new ArrayList<>(mStreamRecorders.keySet()); - - for(Integer streamKey : streamKeys) - { - removeRecorder(streamKey); - } - } - } - } - - @Override - public void receive(ReusableAudioPacket audioPacket) - { - if(mRunning.get() && audioPacket.hasIdentifierCollection()) - { - synchronized(mStreamRecorders) - { - int channelMetadataID = audioPacket.getAudioChannelId(); - - ReusableAudioPacket.Type type = audioPacket.getType(); - - if(type == ReusableAudioPacket.Type.AUDIO) - { - if(mStreamRecorders.containsKey(channelMetadataID)) - { - AudioRecorder recorder = mStreamRecorders.get(channelMetadataID); - - if(recorder != null) - { - audioPacket.incrementUserCount(); - recorder.receive(audioPacket); - } - } - else - { - AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), mBroadcastFormat); - recorder.start(); - audioPacket.incrementUserCount(); - recorder.receive(audioPacket); - mStreamRecorders.put(channelMetadataID, recorder); - } - } - else if(type == ReusableAudioPacket.Type.END) - { - removeRecorder(channelMetadataID); - } - else - { - mLog.info("Unrecognized Audio Packet Type: " + type); - } - } - } - - audioPacket.decrementUserCount(); - } - - /** - * Removes the recorder associated with the source channel ID. - * - * Note: this method invocation is not thread safe and must be invoked by a thread safe mechanism that protects the - * mStreamRecorders map. - * - * @param sourceChannelID identifying the recorder - */ - private void removeRecorder(Integer sourceChannelID) - { - if(mStreamRecorders.containsKey(sourceChannelID)) - { - AudioRecorder recorder = mStreamRecorders.remove(sourceChannelID); - - recorder.close(new Listener() - { - @Override - public void receive(AudioRecorder audioRecorder) - { - AudioRecording audioRecording = - new AudioRecording(audioRecorder.getPath(), audioRecorder.getIdentifierCollection(), - audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength()); - - if(mAudioRecordingListener != null) - { - mAudioRecordingListener.receive(audioRecording); - } - } - }); - - } - } - - /** - * Creates a temporary streaming recording file path - */ - private Path getTemporaryRecordingPath() - { - StringBuilder sb = new StringBuilder(); - sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX); - - int recordingNumber = sNextRecordingNumber.incrementAndGet(); - - //Check for integer overflow and readjust negative value to 0 - if(recordingNumber < 0) - { - sNextRecordingNumber.set(0); - recordingNumber = sNextRecordingNumber.incrementAndGet(); - } - sb.append(recordingNumber).append("_"); - sb.append(TimeStamp.getLongTimeStamp("_")); - sb.append(mBroadcastFormat.getFileExtension()); - - Path temporaryRecordingPath = mUserPreferences.getDirectoryPreference().getDirectoryStreaming().resolve(sb.toString()); - - return temporaryRecordingPath; - } - - /** - * Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording. - */ - public class RecorderMonitor implements Runnable - { - @Override - public void run() - { - synchronized(mStreamRecorders) - { - long now = System.currentTimeMillis(); - - mStreamRecorders.entrySet().stream() - .filter(entry -> entry.getValue().getTimeRecordingStart() + MAXIMUM_RECORDER_LIFESPAN_MILLIS < now) - .forEach(entry -> - { - mLog.info("cycling recorder - max temporary streaming recording time limit reached [" + - entry.getValue().getPath().toString() + "]"); - - removeRecorder(entry.getKey()); - }); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java index 714cddbc2..5e298ae91 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java @@ -22,6 +22,7 @@ package io.github.dsheirer.audio.codec.mbe; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.preference.UserPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +33,9 @@ public abstract class AmbeAudioModule extends JmbeAudioModule private static final String AMBE_CODEC = "AMBE 3600 x 2450"; private static boolean sLibraryStatusLogged = false; - public AmbeAudioModule(UserPreferences userPreferences) + public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList) { - super(userPreferences); + super(userPreferences, aliasList); if(!sLibraryStatusLogged) { diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java index 8a94870e3..35004da5a 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java @@ -22,6 +22,7 @@ package io.github.dsheirer.audio.codec.mbe; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.preference.UserPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +33,9 @@ public abstract class ImbeAudioModule extends JmbeAudioModule private static final String IMBE_CODEC = "IMBE"; private static boolean sLibraryStatusLogged = false; - public ImbeAudioModule(UserPreferences userPreferences) + public ImbeAudioModule(UserPreferences userPreferences, AliasList aliasList) { - super(userPreferences); + super(userPreferences, aliasList); if(!sLibraryStatusLogged) { diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java index 2a27531cc..cc5565e9c 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java @@ -23,14 +23,15 @@ package io.github.dsheirer.audio.codec.mbe; import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.audio.AbstractAudioModule; +import io.github.dsheirer.audio.squelch.ISquelchStateListener; import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessageListener; import io.github.dsheirer.preference.PreferenceType; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue; import jmbe.iface.IAudioCodec; import jmbe.iface.IAudioCodecLibrary; import org.slf4j.Logger; @@ -44,17 +45,18 @@ import java.util.ArrayList; import java.util.List; -public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener, IMessageListener +public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener, IMessageListener, + ISquelchStateListener { private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class); private static final String JMBE_AUDIO_LIBRARY = "JMBE"; private static List mLibraryLoadStatusLogged = new ArrayList<>(); private IAudioCodec mAudioCodec; private UserPreferences mUserPreferences; - private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("JmbeAudioModule"); - public JmbeAudioModule(UserPreferences userPreferences) + public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList) { + super(aliasList); mUserPreferences = userPreferences; MyEventBus.getEventBus().register(this); loadConverter(); @@ -73,11 +75,6 @@ protected boolean hasAudioCodec() return getAudioCodec() != null; } - protected ReusableAudioPacketQueue getAudioPacketQueue() - { - return mAudioPacketQueue; - } - @Override public Listener getMessageListener() { diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java index f879e7288..3dea871fa 100644 --- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java +++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceConverter.java @@ -22,21 +22,9 @@ package io.github.dsheirer.audio.codec.mbe; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.dsheirer.audio.convert.thumbdv.ThumbDv; -import io.github.dsheirer.audio.convert.thumbdv.message.response.AmbeResponse; -import io.github.dsheirer.module.decode.p25.audio.VoiceFrame; -import io.github.dsheirer.record.wave.AudioPacketWaveRecorder; -import io.github.dsheirer.record.wave.WaveMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - /** * Utility for converting MBE call sequences (*.mbe) to PCM wave audio format */ @@ -44,64 +32,64 @@ public class MBECallSequenceConverter { private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class); - public static void convert(Path input, Path output) throws IOException - { - InputStream inputStream = Files.newInputStream(input); - ObjectMapper mapper = new ObjectMapper(); - MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class); - convert(sequence, output); - } - - public static void convert(MBECallSequence callSequence, Path outputPath) - { - if(callSequence == null || callSequence.isEncrypted()) - { - throw new IllegalArgumentException("Cannot decode null or encrypted call sequence"); - } - - if(callSequence != null && !callSequence.isEncrypted()) - { - ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2; - - AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath); - recorder.start(); - - long delayMillis = 0; - - try(ThumbDv thumbDv = new ThumbDv(protocol, recorder)) - { - thumbDv.start(); - for(VoiceFrame voiceFrame: callSequence.getVoiceFrames()) - { - mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]"); - thumbDv.decode(voiceFrame.getFrameBytes()); - delayMillis += 30; - } - - if(delayMillis > 0) - { - delayMillis += 1000; - try - { - Thread.sleep(delayMillis); - } - catch(InterruptedException ie) - { - - } - } - } - catch(IOException ioe) - { - mLog.error("Error", ioe); - } - - recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata()); - } - } - - public static void main(String[] args) - { +// public static void convert(Path input, Path output) throws IOException +// { +// InputStream inputStream = Files.newInputStream(input); +// ObjectMapper mapper = new ObjectMapper(); +// MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class); +// convert(sequence, output); +// } +// +// public static void convert(MBECallSequence callSequence, Path outputPath) +// { +// if(callSequence == null || callSequence.isEncrypted()) +// { +// throw new IllegalArgumentException("Cannot decode null or encrypted call sequence"); +// } +// +// if(callSequence != null && !callSequence.isEncrypted()) +// { +// ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2; +// +// AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath); +// recorder.start(); +// +// long delayMillis = 0; +// +// try(ThumbDv thumbDv = new ThumbDv(protocol, recorder)) +// { +// thumbDv.start(); +// for(VoiceFrame voiceFrame: callSequence.getVoiceFrames()) +// { +// mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]"); +// thumbDv.decode(voiceFrame.getFrameBytes()); +// delayMillis += 30; +// } +// +// if(delayMillis > 0) +// { +// delayMillis += 1000; +// try +// { +// Thread.sleep(delayMillis); +// } +// catch(InterruptedException ie) +// { +// +// } +// } +// } +// catch(IOException ioe) +// { +// mLog.error("Error", ioe); +// } +// +// recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata()); +// } +// } +// +// public static void main(String[] args) +// { // String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_3_TS0_65084_6591007.mbe"; // String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe"; // @@ -118,5 +106,5 @@ public static void main(String[] args) // { // mLog.error("Error", ioe); // } - } +// } } diff --git a/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java b/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java index 2af24280d..52ddb94f8 100644 --- a/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java +++ b/src/main/java/io/github/dsheirer/audio/convert/IAudioConverter.java @@ -18,8 +18,6 @@ ******************************************************************************/ package io.github.dsheirer.audio.convert; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; - import java.util.List; public interface IAudioConverter @@ -27,7 +25,7 @@ public interface IAudioConverter /** * Converts the PCM audio packets to converted audio format. May produce partial audio frame data. */ - public byte[] convert(List audioPackets); + public byte[] convert(List audioBuffers); /** * Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the diff --git a/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java b/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java index 704f13135..176bc1aa4 100644 --- a/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java +++ b/src/main/java/io/github/dsheirer/audio/convert/MP3AudioConverter.java @@ -20,7 +20,6 @@ import io.github.dsheirer.audio.AudioFormats; 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 +53,7 @@ public MP3AudioConverter(int bitRate, boolean variableBitRate) } @Override - public byte[] convert(List audioPackets) + public byte[] convert(List audioPackets) { mMP3Stream.reset(); @@ -84,6 +83,36 @@ public byte[] convert(List audioPackets) } } + public byte[] convertAudio(List audioBuffers) + { + mMP3Stream.reset(); + + byte[] pcmBytes = AudioUtils.convert(audioBuffers); + + int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length); + + int mp3BufferSize = 0; + + int pcmBytesPosition = 0; + + try + { + while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer))) + { + pcmBytesPosition += pcmBufferSize; + pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition); + mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize); + } + + return mMP3Stream.toByteArray(); + } + catch(Exception e) + { + mLog.error("There was an error converting audio to MP3: " + e.getMessage()); + return new byte[0]; + } + } + @Override public byte[] flush() { diff --git a/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java b/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java index 6329db421..8efecc4fd 100644 --- a/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java +++ b/src/main/java/io/github/dsheirer/audio/convert/MP3SilenceGenerator.java @@ -18,9 +18,6 @@ ******************************************************************************/ package io.github.dsheirer.audio.convert; -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; @@ -31,10 +28,11 @@ public class MP3SilenceGenerator implements ISilenceGenerator { private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class); + public static final int MP3_BIT_RATE = 16; + public static final boolean CONSTANT_BIT_RATE = false; - private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE); + private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE); private byte[] mPreviousPartialFrameData; - private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator"); /** * Generates MP3 audio silence frames @@ -46,13 +44,10 @@ public MP3SilenceGenerator() public byte[] generate(long duration) { int length = (int)(duration * 8); //8000 Hz sample rate - ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length); - Arrays.fill(silencePacket.getAudioSamples(), 0.0f); - List silencePackets = new ArrayList<>(); - silencePackets.add(silencePacket); - - byte[] frameData = mGenerator.convert(silencePackets); + List silenceBuffers = new ArrayList<>(); + silenceBuffers.add(new float[length]); + byte[] frameData = mGenerator.convert(silenceBuffers); frameData = merge(mPreviousPartialFrameData, frameData); diff --git a/src/main/java/io/github/dsheirer/audio/convert/thumbdv/ThumbDv.java b/src/main/java/io/github/dsheirer/audio/convert/thumbdv/ThumbDv.java index d791c10f1..3f17a518e 100644 --- a/src/main/java/io/github/dsheirer/audio/convert/thumbdv/ThumbDv.java +++ b/src/main/java/io/github/dsheirer/audio/convert/thumbdv/ThumbDv.java @@ -36,8 +36,8 @@ import io.github.dsheirer.audio.convert.thumbdv.message.response.ReadyResponse; import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; -import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue; +import io.github.dsheirer.sample.buffer.ReusableBufferQueue; +import io.github.dsheirer.sample.buffer.ReusableFloatBuffer; import io.github.dsheirer.util.ThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,14 +82,14 @@ public enum AudioProtocol private ScheduledFuture mAudioDecodeProcessorHandle; private LinkedTransferQueue mDecodeSpeechRequests = new LinkedTransferQueue<>(); private AudioProtocol mAudioProtocol; - private Listener mAudioPacketListener; - private ReusableAudioPacketQueue mReusableAudioPacketQueue = new ReusableAudioPacketQueue("ThumbDv"); + private Listener mAudioBufferListener; + private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ThumbDv"); private boolean mStarted; - public ThumbDv(AudioProtocol audioProtocol, Listener listener) + public ThumbDv(AudioProtocol audioProtocol, Listener listener) { mAudioProtocol = audioProtocol; - mAudioPacketListener = listener; + mAudioBufferListener = listener; } /** @@ -206,12 +206,11 @@ private void receive(byte[] bytes) { AmbeMessage message = AmbeMessageFactory.getMessage(bytes); - if(message instanceof DecodeSpeechResponse && mAudioPacketListener != null) + if(message instanceof DecodeSpeechResponse && mAudioBufferListener != null) { float[] samples = ((DecodeSpeechResponse)message).getSamples(); - ReusableAudioPacket audioPacket = mReusableAudioPacketQueue.getBuffer(samples.length); - audioPacket.loadAudioFrom(samples); - mAudioPacketListener.receive(audioPacket); + ReusableFloatBuffer audioBuffer = mReusableBufferQueue.getBuffer(samples, System.currentTimeMillis()); + mAudioBufferListener.receive(audioBuffer); } else if(message instanceof ReadyResponse && mAudioDecodeProcessorHandle == null) { @@ -457,7 +456,7 @@ public static void main(String[] args) mLog.debug("Starting thumb dv thread(s)"); - final Listener listener = reusableAudioPacket -> { + final Listener listener = reusableAudioPacket -> { mLog.info("Got an audio packet!"); reusableAudioPacket.decrementUserCount(); }; diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java b/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java index b085b2bdb..8b27d937f 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java @@ -127,6 +127,12 @@ public void dispose() { //Deregister from receiving preference update notifications MyEventBus.getEventBus().unregister(this); + + if(mAudioOutput != null) + { + mAudioOutput.removeAudioEventListener(this); + mAudioOutput.removeAudioMetadataListener(); + } } private void init() diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java index f04c315a5..b7e2141e4 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java @@ -1,7 +1,7 @@ /* * * * ****************************************************************************** - * * Copyright (C) 2014-2019 Dennis Sheirer + * * Copyright (C) 2014-2020 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 @@ -21,13 +21,23 @@ */ package io.github.dsheirer.audio.playback; +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.alias.id.priority.Priority; import io.github.dsheirer.audio.AudioEvent; +import io.github.dsheirer.audio.AudioSegment; +import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.IdentifierUpdateNotification; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.preference.UserPreferences; 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 javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,42 +52,39 @@ import javax.sound.sampled.Mixer; import javax.sound.sampled.SourceDataLine; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; -public abstract class AudioOutput implements Listener, LineListener +/** + * Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments + * and broadcasts audio segment metadata to registered listeners (ie gui components). + */ +public abstract class AudioOutput implements LineListener, Listener { private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class); - - private LinkedTransferQueue mBuffer = new LinkedTransferQueue<>(); private int mBufferStartThreshold; private int mBufferStopThreshold; - private static IdentifierCollection EMPTY_IDENTIFIER_COLLECTION = new IdentifierCollection(0); - static - { - EMPTY_IDENTIFIER_COLLECTION.setUpdated(true); - } - private Listener mIdentifierCollectionListener; private Broadcaster mAudioEventBroadcaster = new Broadcaster<>(); - - private ScheduledFuture mProcessorTask; - + private ScheduledFuture mProcessorFuture; private SourceDataLine mOutput; private Mixer mMixer; private MixerChannel mMixerChannel; private FloatControl mGainControl; private BooleanControl mMuteControl; - private AudioEvent mAudioStartEvent; private AudioEvent mAudioStopEvent; - private boolean mCanProcessAudio = false; - private long mLastActivity = System.currentTimeMillis(); + private AudioSegment mCurrentAudioSegment; + private AudioSegment mNextAudioSegment; + private ReentrantLock mLock = new ReentrantLock(); + private int mCurrentBufferIndex = 0; + private UserPreferences mUserPreferences; + private BooleanProperty mEmptyProperty = new SimpleBooleanProperty(true); + private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY); + private ByteBuffer mAudioSegmentStartTone; + private ByteBuffer mAudioSegmentPreemptTone; /** * Single audio channel playback with automatic starting and stopping of the @@ -94,10 +101,11 @@ public abstract class AudioOutput implements Listener, Line * @param requestedBufferSize of approximately 1 second of audio */ public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo, - int requestedBufferSize) + int requestedBufferSize, UserPreferences userPreferences) { mMixer = mixer; mMixerChannel = mixerChannel; + mUserPreferences = userPreferences; try { @@ -139,46 +147,362 @@ public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioForm mixer.getMixerInfo().getName() + " | " + getChannelName() + "]"); } - /* Run the queue processor task every 40 milliseconds or 25 times a second */ - mProcessorTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(), - 0, 40, TimeUnit.MILLISECONDS); + //Run the queue processor task every 250 milliseconds or 4 times a second + mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(), + 0, 250, TimeUnit.MILLISECONDS); } - mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, - getChannelName()); - mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, - getChannelName()); - + mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName()); + mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName()); mCanProcessAudio = true; } } catch(LineUnavailableException e) { - mLog.error("Couldn't obtain audio source data line for " - + "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]"); + mLog.error("Couldn't obtain audio source data line for audio output - mixer [" + + mMixer.getMixerInfo().getName() + "]"); } + + updateToneInsertionAudioClips(); + + //Register to receive directory preference update notifications so we can update the preference items + MyEventBus.getEventBus().register(this); } - public void reset() + /** + * Boolean property that indicates if this audio output has an audio segment queued or in process of playback. + */ + public BooleanProperty emptyProperty() { - broadcast(new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName())); + return mEmptyProperty; } + /** + * Audio playback priority of the current audio segment or default audio priority if nothing is currently loaded. + */ + public IntegerProperty audioPriorityProperty() + { + return mAudioPriority; + } + + /** + * Schedules the audio segment for playback. + * + * Note: if a segment is currently playing and another audio segment is already queued for playback, invoking + * this method will overwrite the queued segment with the argument. + * + * @param audioSegment to schedule for playback. + */ + public void play(AudioSegment audioSegment) + { + if(audioSegment != null) + { + mLock.lock(); + + if(mNextAudioSegment != null) + { + mNextAudioSegment.decrementConsumerCount(); + mNextAudioSegment = null; + } + + try + { + mNextAudioSegment = audioSegment; + mEmptyProperty.set(false); + } + finally + { + mLock.unlock(); + } + } + } + + /** + * Indicates if the audio segment is linked to the current playback audio segment + */ + public boolean isLinkedTo(AudioSegment audioSegment) + { + return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment); + } + + /** + * Receive and process audio identifier update notifications. + */ + @Override + public void receive(IdentifierUpdateNotification identifierUpdateNotification) + { + if(mCurrentAudioSegment != null) + { + IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection(); + + if(identifierCollection != null) + { + broadcast(identifierCollection); + } + } + } + + /** + * Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones. + */ + @Subscribe + public void preferenceUpdated(PreferenceType preferenceType) + { + if(preferenceType == PreferenceType.PLAYBACK) + { + updateToneInsertionAudioClips(); + } + } + + /** + * Updates audio segment start and preempt tone insertion clips + */ + private void updateToneInsertionAudioClips() + { + mAudioSegmentStartTone = null; + mAudioSegmentPreemptTone = null; + + float[] start = mUserPreferences.getPlaybackPreference().getStartTone(); + + if(start != null) + { + mAudioSegmentStartTone = convert(start); + } + + float[] preempt = mUserPreferences.getPlaybackPreference().getPreemptTone(); + + if(start != null) + { + mAudioSegmentPreemptTone = convert(preempt); + } + } + + /** + * Disposes of a processed audio segment and notifies the originator that processing of the segment is complete + * by decrementing the consumer count. + * @param audioSegment to dispose + */ + private void dispose(AudioSegment audioSegment) + { + mAudioPriority.unbind(); + + if(audioSegment != null) + { + audioSegment.decrementConsumerCount(); + audioSegment.removeIdentifierUpdateNotificationListener(this); + } + } + + /** + * Generates a tone indicating that a new audio segment is starting + */ + private ByteBuffer getAudioSegmentStartTone() + { + return mAudioSegmentStartTone; + } + + /** + * Generates a tone indicating that the current audio segment playback has been preempted for a higher priority + * audio segment that is now starting. + */ + private ByteBuffer getAudioSegmentPreemptionTone() + { + return mAudioSegmentPreemptTone; + } + + /** + * Writes the audio buffer data to the source data line. If the data line is not currently playing, write as + * much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual + * buffer content to the data line as a blocking call. + * + * @param buffer of audio to playback + */ + private void playAudio(ByteBuffer buffer) + { + if(buffer != null) + { + int wrote = 0; + + //If the output data line is not running, we can only write up to the available capacity. So, only write + //what will fit initially, start playback, and then use a blocking write for the remainder. + if(!mOutput.isRunning()) + { + int toWrite = mOutput.available(); + + if(toWrite > buffer.array().length) + { + toWrite = buffer.array().length; + } + + //Top off the buffer and check if we can start it + wrote += mOutput.write(buffer.array(), 0, toWrite); + + checkStart(); + } + + if(mOutput.isRunning() && wrote < buffer.array().length) + { + //This will block until the buffer is fully written to the data line + mOutput.write(buffer.array(), wrote, buffer.array().length - wrote); + } + } + } + + /** + * Manage audio segment playback and process audio segment buffers. This method is designed to be called + * by a threaded processor repeatedly to playback the current audio segment and check for and start a newly + * assigned audio segment. It also handles starting and stopping the playback source data line to avoid audio + * discontinuities due to buffer underruns. + */ + private void processAudio() + { + if(mNextAudioSegment != null) + { + mLock.lock(); + + try + { + //For linked audio segments, allow the linked segment to complete first before assigning the next + if(mNextAudioSegment.isLinked()) + { + if(mCurrentAudioSegment == null) + { + mCurrentAudioSegment = mNextAudioSegment; + mNextAudioSegment = null; + mCurrentBufferIndex = 0; + + if(mCurrentAudioSegment != null) + { + mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty()); + mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this); + broadcast(mCurrentAudioSegment.getIdentifierCollection()); + } + else + { + mAudioPriority.setValue(Priority.DEFAULT_PRIORITY); + } + } + } + else + { + //Insert audio segment start or audio priority preemption bonk tone + if(mCurrentAudioSegment == null) + { + playAudio(getAudioSegmentStartTone()); + } + else if(mCurrentBufferIndex > 0 && + (!mCurrentAudioSegment.completeProperty().get() || + mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())) + { + playAudio(getAudioSegmentPreemptionTone()); + } + else + { + playAudio(getAudioSegmentStartTone()); + } + + //Close current audio segment + dispose(mCurrentAudioSegment); + mCurrentAudioSegment = mNextAudioSegment; + mNextAudioSegment = null; + mCurrentBufferIndex = 0; + + if(mCurrentAudioSegment != null) + { + mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty()); + mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this); + broadcast(mCurrentAudioSegment.getIdentifierCollection()); + } + else + { + mAudioPriority.setValue(Priority.DEFAULT_PRIORITY); + } + } + } + finally + { + mLock.unlock(); + } + } + + if(mCurrentAudioSegment != null) + { + //Check for completed audio segment + if(mCurrentAudioSegment.completeProperty().get() && + mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()) + { + dispose(mCurrentAudioSegment); + mCurrentAudioSegment = null; + + mLock.lock(); + + try + { + if(mNextAudioSegment == null) + { + mEmptyProperty.set(true); + } + } + finally + { + mLock.unlock(); + } + + return; + } + + //Process any new buffers that have been added to the audio segment. If a next audio segment gets assigned + //while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next + //segment is a linked segment, ignore it so that we can close out the current segment. + while((mNextAudioSegment == null || mNextAudioSegment.isLinked()) && + mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount()) + { + float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++); + + if(audioBuffer != null) + { + ByteBuffer audio = convert(audioBuffer); + playAudio(audio); + } + } + } + + checkStop(); + } + + /** + * Prepares this audio output for disposal. + */ public void dispose() { mCanProcessAudio = false; - if(mProcessorTask != null) + if(mProcessorFuture != null) { - mProcessorTask.cancel(true); + mProcessorFuture.cancel(true); } - mProcessorTask = null; + mProcessorFuture = null; + + mLock.lock(); + + try + { + if(mNextAudioSegment != null) + { + mNextAudioSegment.decrementConsumerCount(); + mNextAudioSegment = null; + } + } + finally + { + mLock.unlock(); + } - mBuffer.clear(); + dispose(mCurrentAudioSegment); + mCurrentAudioSegment = null; - mAudioEventBroadcaster.dispose(); - mAudioEventBroadcaster = null; + mAudioEventBroadcaster.clear(); mIdentifierCollectionListener = null; if(mOutput != null) @@ -192,10 +516,9 @@ public void dispose() } /** - * Converts the audio packet data into a byte buffer format appropriate for - * the underlying source data line. + * Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line. */ - protected abstract ByteBuffer convert(ReusableAudioPacket packet); + protected abstract ByteBuffer convert(float[] buffer); /** * Audio output channel name @@ -229,7 +552,7 @@ public void removeAudioEventListener(Listener listener) /** * Broadcasts an audio event to the registered listener */ - private void broadcast(AudioEvent audioEvent) + private void broadcastAudioEvent(AudioEvent audioEvent) { mAudioEventBroadcaster.broadcast(audioEvent); } @@ -243,6 +566,9 @@ public void setIdentifierCollectionListener(Listener liste mIdentifierCollectionListener = listener; } + /** + * Unregisters the current audio metadata listener + */ public void removeAudioMetadataListener() { mIdentifierCollectionListener = null; @@ -260,139 +586,31 @@ private void broadcast(IdentifierCollection identifierCollection) } /** - * Timestamp of either last buffer received or last buffer processed - */ - public long getLastActivityTimestamp() - { - return mLastActivity; - } - - /** - * Updates the last activity timestamp to current system time + * Starts audio playback once audio buffer is almost full and remaining capacity falls below the start threshold. + * + * Note: this method should only be invoked from the processAudio() method */ - public void updateTimestamp() - { - mLastActivity = System.currentTimeMillis(); - } - - @Override - public void receive(ReusableAudioPacket packet) + private void checkStart() { - if(mCanProcessAudio) - { - //Update the activity timestamp so that this audio output doesn't - //get disconnected before it starts processing the audio stream - updateTimestamp(); - - mBuffer.add(packet); - } - else + if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold) { - packet.decrementUserCount(); + mOutput.start(); } } - public class BufferProcessor implements Runnable + /** + * Stops audio playback and drains the audio buffer to empty when the audio buffer is mostly empty and the + * available buffer capacity exceeds the stop threshold + * + * Note: this method should only be invoked from the processAudio() method + */ + private void checkStop() { - private AtomicBoolean mProcessing = new AtomicBoolean(); - private List mAudioPackets = new ArrayList(); - - - public BufferProcessor() - { - } - - @Override - public void run() - { - try - { - /* The processing flag ensures that only one instance of the - * processor can run at any given time */ - if(mProcessing.compareAndSet(false, true)) - { - mBuffer.drainTo(mAudioPackets); - - for(ReusableAudioPacket packet : mAudioPackets) - { - if(packet.getType() == ReusableAudioPacket.Type.AUDIO) - { - broadcast(packet.getIdentifierCollection()); - - ByteBuffer buffer = convert(packet); - - int wrote = 0; - - if(!mOutput.isRunning()) - { - int toWrite = mOutput.available(); - - if(toWrite > buffer.array().length) - { - toWrite = buffer.array().length; - } - - //Top off the buffer and check if we can start it - wrote += mOutput.write(buffer.array(), 0, toWrite); - - checkStart(); - } - - if(mOutput.isRunning() && wrote < buffer.array().length) - { - //Blocking write - wrote += mOutput.write(buffer.array(), wrote, - buffer.array().length - wrote); - } - - updateTimestamp(); - } - else - { - packet.decrementUserCount(); - } - } - - mAudioPackets.clear(); - - checkStop(); - - mProcessing.set(false); - } - } - catch(Exception e) - { - mLog.error("Error while processing audio buffers", e); - } - } - - /** - * Starts audio playback once audio buffer is almost full and remaining - * capacity falls below the start threshold. - */ - private void checkStart() + if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold) { - if(mCanProcessAudio && - !mOutput.isRunning() && - mOutput.available() <= mBufferStartThreshold) - { - mOutput.start(); - } - } - - /** - * Stops audio playback and drains the audio buffer to empty when the - * audio buffer is mostly empty and the available buffer capacity - * exceeds the stop threshold - */ - private void checkStop() - { - if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold) - { - mOutput.drain(); - mOutput.stop(); - broadcast(EMPTY_IDENTIFIER_COLLECTION); - } + mOutput.drain(); + mOutput.stop(); + broadcast(null); } } @@ -404,9 +622,7 @@ public void setMuted(boolean muted) if(mMuteControl != null) { mMuteControl.setValue(muted); - - broadcast(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : - AudioEvent.Type.AUDIO_UNMUTED, getChannelName())); + broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName())); } } @@ -431,14 +647,17 @@ public FloatControl getGainControl() return mGainControl; } + /** + * Indicates if this audio output has a gain control available + */ public boolean hasGainControl() { return mGainControl != null; } /** - * Monitors the source data line playback state and broadcasts audio events - * to the registered listener as the state changes + * Monitors the source data line playback state and broadcasts audio events to the registered listener as the + * state changes */ @Override public void update(LineEvent event) @@ -454,4 +673,23 @@ else if(type == LineEvent.Type.STOP) mAudioEventBroadcaster.broadcast(mAudioStopEvent); } } + + /** + * Runnable audio segment processor + */ + public class AudioSegmentProcessor implements Runnable + { + @Override + public void run() + { + try + { + processAudio(); + } + catch(Throwable t) + { + mLog.error("Error while processing audio buffers", t); + } + } + } } diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java index 5b2333f4a..5f6195996 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java @@ -23,17 +23,24 @@ import io.github.dsheirer.audio.AudioEvent; import io.github.dsheirer.audio.AudioException; import io.github.dsheirer.audio.IAudioController; +import io.github.dsheirer.eventbus.MyEventBus; +import io.github.dsheirer.gui.preference.PreferenceEditorType; +import io.github.dsheirer.gui.preference.PreferenceEditorViewRequest; import io.github.dsheirer.icon.IconManager; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.SourceManager; import io.github.dsheirer.source.mixer.MixerChannelConfiguration; +import io.github.dsheirer.source.mixer.MixerManager; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.swing.IconFontSwing; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sound.sampled.FloatControl; +import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JMenu; @@ -51,6 +58,7 @@ import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; +import java.util.List; public class AudioPanel extends JPanel implements Listener { @@ -169,34 +177,19 @@ public void mouseClicked(MouseEvent event) JPopupMenu popup = new JPopupMenu(); /* Audio mixer/output selection menus */ - JMenu outputMenu = new JMenu("Audio Output"); - - MixerChannelConfiguration[] mixerConfigurations = - mSourceManager.getMixerManager().getOutputMixers(); - - for(MixerChannelConfiguration mixerConfig : mixerConfigurations) + JMenuItem outputMenu = new JMenuItem("Configure ..."); + Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14); + outputMenu.setIcon(icon); + outputMenu.addActionListener(new ActionListener() { - MixerSelectionItem mixerItem = new MixerSelectionItem(mixerConfig); - - try - { - MixerChannelConfiguration current = mController.getMixerChannelConfiguration(); - - if(current != null && current.equals(mixerConfig)) - { - mixerItem.setSelected(true); - } - } - catch(AudioException e) + @Override + public void actionPerformed(ActionEvent e) { - mLog.error("Error while detecting current mixer " - + "channel configuration", e); + MyEventBus.getEventBus().post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK)); } - - outputMenu.add(mixerItem); - } - + }); popup.add(outputMenu); + popup.add(new JPopupMenu.Separator()); /* Audio output mute and volume control */ for(AudioOutput output : mController.getAudioOutputs()) diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java index 449354d6d..62ea4515d 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java @@ -19,13 +19,17 @@ */ package io.github.dsheirer.audio.playback; +import com.google.common.eventbus.Subscribe; import io.github.dsheirer.audio.AudioEvent; import io.github.dsheirer.audio.AudioException; +import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.audio.IAudioController; +import io.github.dsheirer.eventbus.MyEventBus; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.preference.UserPreferences; 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; @@ -38,182 +42,192 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class AudioPlaybackManager implements Listener, IAudioController +/** + * Manages scheduling and playback of audio segments to the local users audio system. + */ +public class AudioPlaybackManager implements Listener, IAudioController { private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class); - public static final int AUDIO_TIMEOUT = 1000; //1 second - public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels"; public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer"; - public static final AudioEvent CONFIGURATION_CHANGE_STARTED = new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null); - public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE = new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null); - - private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>(); - private Map mChannelConnectionMap = new HashMap<>(); - private List mAudioOutputConnections = new ArrayList<>(); - private AudioOutputConnection mLowestPriorityConnection; - private int mAvailableConnectionCount; - - private Map mAudioOutputMap = new HashMap<>(); - private Broadcaster mControllerBroadcaster = new Broadcaster<>(); - private ScheduledFuture mProcessingTask; - private MixerManager mMixerManager; + private UserPreferences mUserPreferences; private MixerChannelConfiguration mMixerChannelConfiguration; - + private List mAudioOutputs = new ArrayList<>(); + private List mAudioSegments = new ArrayList<>(); + private LinkedTransferQueue mNewAudioSegmentQueue = new LinkedTransferQueue<>(); /** - * Processes all audio produced by the decoding channels and routes audio - * packets to any combination of outputs based on any alias audio routing - * options specified by the user. + * Constructs an instance. + * + * @param userPreferences for audio playback preferences */ - public AudioPlaybackManager(MixerManager mixerManager) + public AudioPlaybackManager(UserPreferences userPreferences) { - mMixerManager = mixerManager; + mUserPreferences = userPreferences; + MyEventBus.getEventBus().register(this); - loadSettings(); + MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration(); + try + { + setMixerChannelConfiguration(configuration); + } + catch(AudioException ae) + { + mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" + + (configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" + + (configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae); + } } /** - * Loads the saved mixer configuration or a default configuration for audio playback. + * Receives audio segments from channel audio modules. + * @param audioSegment */ - private void loadSettings() + @Override + public void receive(AudioSegment audioSegment) { - MixerChannelConfiguration configuration = null; - - SystemProperties properties = SystemProperties.getInstance(); + mNewAudioSegmentQueue.add(audioSegment); + } - Mixer defaultMixer = AudioSystem.getMixer(null); + /** + * Processes new audio segments and automatically assigns them to audio outputs. + * + * Note: this method is intended to be repeatedly invoked by a scheduled processing thread. + */ + private void processAudioSegments() + { + //Transfer new audio segments out of the concurrent queue and into the managed segments list. + mNewAudioSegmentQueue.drainTo(mAudioSegments); - String mixer = properties.get(AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName()); + if(!mAudioSegments.isEmpty()) + { + Iterator it = mAudioSegments.iterator(); + AudioSegment audioSegment; - String channels = properties.get(AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name()); + //Remove any audio segments flagged as do not monitor. Don't remove completed segments yet, because + //we want to give them a brief chance at playback. Automatically assign linked audio segments to the + //current audio output for audio continutity + while(it.hasNext()) + { + audioSegment = it.next(); - MixerChannelConfiguration[] mixerConfigurations = mMixerManager.getOutputMixers(); + if(audioSegment.isDoNotMonitor()) + { + audioSegment.decrementConsumerCount(); + it.remove(); + } + else if(audioSegment.isLinked()) + { + for(AudioOutput audioOutput: mAudioOutputs) + { + if(audioOutput.isLinkedTo(audioSegment)) + { + audioOutput.play(audioSegment); + it.remove(); + } + } + } + } - for(MixerChannelConfiguration mixerConfig : mixerConfigurations) - { - if(mixerConfig.matches(mixer, channels)) + //Assign audio segments to empty audio outputs + if(!mAudioSegments.isEmpty()) { - configuration = mixerConfig; - } - } + Collections.sort(mAudioSegments, Comparator.comparingInt(o -> o.monitorPriorityProperty().get())); - if(configuration == null) - { - configuration = getDefaultConfiguration(); - } + //Assign empty audio outputs first + for(AudioOutput audioOutput: mAudioOutputs) + { + if(audioOutput.emptyProperty().get()) + { + audioOutput.play(mAudioSegments.remove(0)); - try - { - setMixerChannelConfiguration(configuration, false); - } - catch(Exception e) - { - mLog.error("Couldn't set stored audio mixer/channel configuration - using default", e); + if(mAudioSegments.isEmpty()) + { + return; + } + } + } + } - try + //Preempt ongoing audio segment playback when higher priority audio segments are available + if(!mAudioSegments.isEmpty()) { - setMixerChannelConfiguration(getDefaultConfiguration()); + //Assign empty audio outputs first + for(AudioOutput audioOutput: mAudioOutputs) + { + if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get()) + { + audioOutput.play(mAudioSegments.remove(0)); + + if(mAudioSegments.isEmpty()) + { + return; + } + } + } } - catch(Exception e2) + + //Remove any audio segments marked as completed + it = mAudioSegments.iterator(); //reset the iterator + while(it.hasNext()) { - mLog.error("Couldn't set default audio mixer/channel configuration - no audio will be available", e2); + audioSegment = it.next(); + + if(audioSegment.completeProperty().get()) + { + it.remove(); + audioSegment.decrementConsumerCount(); + } } } } - /** - * Creates a default audio playback configuration with a mono audio playback channel. - */ - private MixerChannelConfiguration getDefaultConfiguration() - { - /* Use the system default mixer and mono channel as default startup */ - Mixer defaultMixer = AudioSystem.getMixer(null); - - return new MixerChannelConfiguration(defaultMixer, MixerChannel.MONO); - } - public void dispose() { if(mProcessingTask != null) { mProcessingTask.cancel(true); + mProcessingTask = null; } - mAudioPacketQueue.clear(); - - mProcessingTask = null; - - mChannelConnectionMap.clear(); - - for(AudioOutputConnection connection : mAudioOutputConnections) - { - connection.dispose(); - } - - mAudioOutputConnections.clear(); + mNewAudioSegmentQueue.clear(); + mAudioSegments.clear(); } /** - * Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices. + * Receive user preference update notifications so that we can detect when the user changes the audio output + * device in the user preferences editor. */ - @Override - public synchronized void receive(ReusableAudioPacket packet) + @Subscribe + public void preferenceUpdated(PreferenceType preferenceType) { - mAudioPacketQueue.add(packet); - } - - /** - * Checks each audio channel assignment and disconnects any inactive connections - */ - private void disconnectInactiveChannelAssignments() - { - boolean changed = false; - - for(AudioOutputConnection connection : mAudioOutputConnections) - { - if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getAudioChannelId())) - { - mChannelConnectionMap.remove(connection.getAudioChannelId()); - connection.disconnect(); - mAvailableConnectionCount++; - changed = true; - } - } - - if(changed) + if(preferenceType == PreferenceType.PLAYBACK) { - updateLowestPriorityAssignment(); - } - } - - /** - * Identifies the lowest priority channel connection where the a higher value indicates a lower priority. - */ - private void updateLowestPriorityAssignment() - { - mLowestPriorityConnection = null; + MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration(); - for(AudioOutputConnection connection : mAudioOutputConnections) - { - if(connection.isConnected() && - (mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() < connection.getPriority())) + if(configuration != null && !configuration.equals(mMixerChannelConfiguration)) { - mLowestPriorityConnection = connection; + try + { + setMixerChannelConfiguration(configuration); + } + catch(AudioException ae) + { + mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae); + } } } } @@ -227,19 +241,7 @@ private void updateLowestPriorityAssignment() @Override public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException { - setMixerChannelConfiguration(entry, true); - } - - /** - * Configures audio playback to use the configuration specified in the entry argument. - * - * @param entry to use in configuring the audio playback setup. - * @param saveSettings to save the audio playback configuration settings in the properties file. - * @throws AudioException if there is an error - */ - public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException - { - if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO)) + if(entry != null) { mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED); @@ -248,68 +250,37 @@ public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolea mProcessingTask.cancel(true); } - disposeCurrentConfiguration(); + for(AudioOutput audioOutput: mAudioOutputs) + { + audioOutput.dispose(); + } + + mAudioOutputs.clear(); switch(entry.getMixerChannel()) { case MONO: - AudioOutput mono = new MonoAudioOutput(entry.getMixer()); - mAudioOutputConnections.add(new AudioOutputConnection(mono)); - mAvailableConnectionCount++; - mAudioOutputMap.put(mono.getChannelName(), mono); + AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences); + mAudioOutputs.add(mono); break; case STEREO: - AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT); - mAudioOutputConnections.add(new AudioOutputConnection(left)); - mAvailableConnectionCount++; - mAudioOutputMap.put(left.getChannelName(), left); - - AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT); - mAudioOutputConnections.add(new AudioOutputConnection(right)); - mAvailableConnectionCount++; - mAudioOutputMap.put(right.getChannelName(), right); + AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences); + mAudioOutputs.add(left); + + AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences); + mAudioOutputs.add(right); break; default: - throw new AudioException("Unsupported mixer channel " - + "configuration: " + entry.getMixerChannel()); + throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel()); } - mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioPacketProcessor(), - 0, 15, TimeUnit.MILLISECONDS); - + mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(), + 0, 250, TimeUnit.MILLISECONDS); mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE); - - if(saveSettings) - { - SystemProperties properties = SystemProperties.getInstance(); - properties.set(AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName()); - properties.set(AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name()); - } + mMixerChannelConfiguration = entry; } } - /** - * Clears all channel assignments and terminates all audio outputs in preparation for complete shutdown or change - * to another mixer/channel configuration - */ - private void disposeCurrentConfiguration() - { - mChannelConnectionMap.clear(); - - for(AudioOutputConnection connection : mAudioOutputConnections) - { - connection.dispose(); - } - - mAvailableConnectionCount = 0; - - mAudioOutputConnections.clear(); - - mAudioOutputMap.clear(); - - mLowestPriorityConnection = null; - } - /** * Current audio playback mixer channel configuration setting. */ @@ -320,12 +291,12 @@ public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioExce } /** - * List of audio outputs available for the current mixer channel configuration + * List of sorted audio outputs available for the current mixer channel configuration */ @Override public List getAudioOutputs() { - List outputs = new ArrayList<>(mAudioOutputMap.values()); + List outputs = new ArrayList<>(mAudioOutputs); Collections.sort(outputs, new Comparator() { @@ -358,228 +329,21 @@ public void removeControllerListener(Listener listener) } /** - * Returns an audio output connection for the packet if one is available, or overrides an existing lower priority - * connection. Returns null if no connection is available for the audio packet. - * - * @param audioPacket from a decoding channel source - * @return an audio output connection or null + * Scheduled runnable to process incoming audio segments */ - private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket) - { - int audioChannelId = audioPacket.getAudioChannelId(); - - //Use an existing connection - if(mChannelConnectionMap.containsKey(audioChannelId)) - { - return mChannelConnectionMap.get(audioChannelId); - } - - //Connect to an unused, available connection - if(mAvailableConnectionCount > 0) - { - for(AudioOutputConnection connection : mAudioOutputConnections) - { - if(connection.isDisconnected()) - { - connection.connect(audioChannelId, audioPacket.getMonitoringPriority()); - mChannelConnectionMap.put(audioChannelId, connection); - mAvailableConnectionCount--; - return connection; - } - } - } - //Preempt an existing lower priority connection and connect when this is a higher priority packet - else - { - int priority = audioPacket.getMonitoringPriority(); - - AudioOutputConnection connection = mLowestPriorityConnection; - - if(connection != null && priority < connection.getPriority()) - { - mChannelConnectionMap.remove(connection.getAudioChannelId()); - - connection.connect(audioChannelId, priority); - - mChannelConnectionMap.put(audioChannelId, connection); - - return connection; - } - } - - return null; - } - - public class AudioPacketProcessor implements Runnable + public class AudioSegmentProcessor implements Runnable { - private List mPackets = new ArrayList<>(); - @Override public void run() { try { - disconnectInactiveChannelAssignments(); - - if(mAudioPacketQueue != null) - { - mAudioPacketQueue.drainTo(mPackets); - - for(ReusableAudioPacket packet : mPackets) - { - /* Don't process any packet's marked as do not monitor */ - if(!packet.isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO) - { - AudioOutputConnection connection = getConnection(packet); - - if(connection != null) - { - connection.receive(packet); - } - else - { - packet.decrementUserCount(); - } - } - else - { - packet.decrementUserCount(); - } - } - - mPackets.clear(); - } + processAudioSegments(); } - catch(Exception e) + catch(Throwable t) { - mLog.error("Encountered error while processing audio packets", e); - - while(mPackets.size() > 0) - { - ReusableAudioPacket audioPacket = mPackets.remove(0); - audioPacket.decrementUserCount(); - } + mLog.error("Encountered error while processing audio segments", t); } } } - - /** - * Audio output connection manages a connection between a source and an audio - * output and maintains current state information about the audio activity - * received form the source. - */ - public class AudioOutputConnection - { - private static final int DISCONNECTED = Integer.MIN_VALUE; - - private AudioOutput mAudioOutput; - private int mPriority = 0; - private int mAudioChannelId = DISCONNECTED; - - public AudioOutputConnection(AudioOutput audioOutput) - { - mAudioOutput = audioOutput; - } - - public void receive(ReusableAudioPacket packet) - { - if(packet.hasIdentifierCollection() && packet.getAudioChannelId() == mAudioChannelId) - { - int priority = packet.getMonitoringPriority(); - - if(mPriority != priority) - { - mPriority = priority; - updateLowestPriorityAssignment(); - } - - if(mAudioOutput != null) - { - mAudioOutput.receive(packet); - } - } - else - { - if(packet.hasIdentifierCollection()) - { - mLog.error("Received audio packet from channel metadata [" + packet.getAudioChannelId() + - "] however this assignment is currently connected to metadata [" + mAudioChannelId + "]"); - } - else - { - mLog.error("Received audio packet with no metadata - cannot route audio packet"); - } - } - } - - /** - * Terminates the audio output and prepares this connection for disposal - */ - public void dispose() - { - mAudioOutput.dispose(); - mAudioOutput = null; - } - - /** - * Indicates if this assignment is currently disconnected from a channel source - */ - public boolean isDisconnected() - { - return mAudioChannelId == DISCONNECTED; - } - - /** - * Indicates if this assignment is currently connected to a channel source - */ - public boolean isConnected() - { - return !isDisconnected(); - } - - /** - * Connects this assignment to the indicated source so that audio - * packets from this source can be sent to the audio output - */ - public void connect(int source, int priority) - { - mAudioChannelId = source; - mPriority = priority; - updateLowestPriorityAssignment(); - - mAudioOutput.updateTimestamp(); - } - - /** - * Currently connected source or -1 if disconnected - */ - public int getAudioChannelId() - { - return mAudioChannelId; - } - - /** - * Disconnects this assignment from the source and prevents any audio - * from being routed to the audio output until another source is assigned - */ - public void disconnect() - { - mAudioChannelId = DISCONNECTED; - mPriority = 0; - } - - /** - * Indicates if audio output is current inactive, meaning that the - * audio output hasn't recently processed any audio packets. - */ - public boolean isInactive() - { - return (mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT) < System.currentTimeMillis(); - } - - public int getPriority() - { - return mPriority; - } - } } diff --git a/src/main/java/io/github/dsheirer/audio/playback/MonoAudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/MonoAudioOutput.java index 2fad8bfc9..ac6972f30 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/MonoAudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/MonoAudioOutput.java @@ -19,7 +19,7 @@ package io.github.dsheirer.audio.playback; import io.github.dsheirer.audio.AudioFormats; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; +import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.source.mixer.MixerChannel; import javax.sound.sampled.Mixer; @@ -34,23 +34,21 @@ public class MonoAudioOutput extends AudioOutput { private final static int BUFFER_SIZE = 8000; - public MonoAudioOutput(Mixer mixer) + public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences) { super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, - AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE); + AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences); } /** * Converts the audio packet data into mono audio frames. */ - protected ByteBuffer convert(ReusableAudioPacket packet) + protected ByteBuffer convert(float[] samples) { ByteBuffer buffer = null; - if(packet.hasAudioSamples()) + if(samples.length > 0) { - float[] samples = packet.getAudioSamples(); - /* Little-endian byte buffer */ buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN); @@ -62,8 +60,6 @@ protected ByteBuffer convert(ReusableAudioPacket packet) } } - packet.decrementUserCount(); - return buffer; } } diff --git a/src/main/java/io/github/dsheirer/audio/playback/StereoAudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/StereoAudioOutput.java index c8538c7ff..70f773057 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/StereoAudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/StereoAudioOutput.java @@ -19,7 +19,7 @@ package io.github.dsheirer.audio.playback; import io.github.dsheirer.audio.AudioFormats; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; +import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.source.mixer.MixerChannel; import javax.sound.sampled.Mixer; @@ -34,10 +34,10 @@ public class StereoAudioOutput extends AudioOutput { private final static int BUFFER_SIZE = 16000; - public StereoAudioOutput(Mixer mixer, MixerChannel channel) + public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences) { super(mixer, channel, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO, - BUFFER_SIZE); + BUFFER_SIZE, userPreferences); } /** @@ -45,14 +45,12 @@ public StereoAudioOutput(Mixer mixer, MixerChannel channel) * channel containing the audio and the other channel containing zero * valued (silent) samples. */ - protected ByteBuffer convert(ReusableAudioPacket packet) + protected ByteBuffer convert(float[] samples) { ByteBuffer buffer = null; - if(packet.hasAudioSamples()) + if(samples.length > 0) { - float[] samples = packet.getAudioSamples(); - /* Little-endian byte buffer */ buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN); @@ -76,8 +74,6 @@ protected ByteBuffer convert(ReusableAudioPacket packet) } } - packet.decrementUserCount(); - return buffer; } } diff --git a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java index 310046b81..f594003c7 100644 --- a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java +++ b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java @@ -1,29 +1,33 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.channel.state; import io.github.dsheirer.channel.state.DecoderStateEvent.Event; +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.string.SimpleStringIdentifier; +import io.github.dsheirer.identifier.string.StringIdentifier; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.protocol.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +39,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState { private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class); private static final String NO_SQUELCH = "No Squelch"; + private Identifier mChannelNameIdentifier; private DecoderType mDecoderType; private String mChannelName; @@ -42,7 +47,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName) { mDecoderType = decoderType; - mChannelName = channelName; + mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : decoderType.name() + " CHANNEL"; + mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.USER, Form.CHANNEL_NAME, Role.TO); } @Override @@ -74,9 +80,7 @@ public void receiveDecoderStateEvent(DecoderStateEvent event) { if(event.getEvent() == Event.RESET) { - //Each time we're reset, set the PRIMARY TO attribute back to the channel name, otherwise we won't have - //a primary ID for any audio produced by this state. -// broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH)); + getIdentifierCollection().update(mChannelNameIdentifier); } } @@ -89,8 +93,8 @@ public DecoderType getDecoderType() @Override public void start() { -// broadcast(new AttributeChangeRequest(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH)); broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE)); + getIdentifierCollection().update(mChannelNameIdentifier); } @Override diff --git a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java index 248073290..5fe00715f 100644 --- a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java +++ b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.controller; @@ -37,7 +34,6 @@ import io.github.dsheirer.map.MapPanel; import io.github.dsheirer.map.MapService; import io.github.dsheirer.preference.UserPreferences; -import io.github.dsheirer.record.RecorderManager; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.SourceManager; import io.github.dsheirer.source.tuner.TunerModel; @@ -67,8 +63,7 @@ public class ControllerPanel extends JPanel public ControllerPanel(AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel, BroadcastModel broadcastModel, ChannelModel channelModel, ChannelMapModel channelMapModel, ChannelProcessingManager channelProcessingManager, IconManager iconManager, MapService mapService, SettingsManager settingsManager, - SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences, - RecorderManager recorderManager) + SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences) { mBroadcastModel = broadcastModel; mChannelModel = channelModel; @@ -88,7 +83,7 @@ public ControllerPanel(AudioPlaybackManager audioPlaybackManager, AliasModel ali mAliasController = new AliasController(aliasModel, broadcastModel, iconManager, userPreferences); - mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences, recorderManager); + mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences); init(); } diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index 0d45ee7e1..b0ac2b95a 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -22,6 +22,7 @@ package io.github.dsheirer.controller.channel; import io.github.dsheirer.alias.AliasModel; +import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.channel.metadata.ChannelMetadata; import io.github.dsheirer.channel.metadata.ChannelMetadataModel; @@ -42,10 +43,8 @@ import io.github.dsheirer.module.log.EventLogManager; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.record.RecorderFactory; -import io.github.dsheirer.record.RecorderManager; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import io.github.dsheirer.source.Source; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceException; @@ -71,14 +70,13 @@ public class ChannelProcessingManager implements Listener private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE"; private Map mProcessingChains = new HashMap<>(); - private List> mAudioPacketListeners = new CopyOnWriteArrayList<>(); + private List> mAudioSegmentListeners = new CopyOnWriteArrayList<>(); private List> mDecodeEventListeners = new CopyOnWriteArrayList<>(); private Broadcaster mChannelEventBroadcaster = new Broadcaster(); private ChannelMapModel mChannelMapModel; private ChannelMetadataModel mChannelMetadataModel; private EventLogManager mEventLogManager; - private RecorderManager mRecorderManager; private SourceManager mSourceManager; private AliasModel mAliasModel; private UserPreferences mUserPreferences; @@ -88,18 +86,15 @@ public class ChannelProcessingManager implements Listener * * @param channelMapModel containing channel maps defined by the user * @param eventLogManager for adding event loggers to channels - * @param recorderManager for receiving audio packets produced by the channel * @param sourceManager for obtaining a tuner channel source for the channel * @param aliasModel for aliasing of identifiers produced by the channel * @param userPreferences for user defined behavior and settings */ public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager, - RecorderManager recorderManager, SourceManager sourceManager, - AliasModel aliasModel, UserPreferences userPreferences) + SourceManager sourceManager, AliasModel aliasModel, UserPreferences userPreferences) { mChannelMapModel = channelMapModel; mEventLogManager = eventLogManager; - mRecorderManager = recorderManager; mSourceManager = sourceManager; mAliasModel = aliasModel; mUserPreferences = userPreferences; @@ -253,9 +248,9 @@ private void startProcessing(ChannelEvent event) mChannelEventBroadcaster.addListener(processingChain); /* Register global listeners */ - for(Listener listener : mAudioPacketListeners) + for(Listener listener : mAudioSegmentListeners) { - processingChain.addAudioPacketListener(listener); + processingChain.addAudioSegmentListener(listener); } for(Listener listener : mDecodeEventListeners) @@ -314,7 +309,7 @@ private void startProcessing(ChannelEvent event) } //Add recorders - processingChain.addModules(RecorderFactory.getRecorders(mRecorderManager, mUserPreferences, channel)); + processingChain.addModules(RecorderFactory.getRecorders(mUserPreferences, channel)); //Set the samples source processingChain.setSource(source); @@ -423,13 +418,11 @@ private void stopProcessing(Channel channel, boolean remove) */ public void shutdown() { - mLog.debug("Stopping Channels ..."); - List channelsToStop = new ArrayList<>(mProcessingChains.keySet()); for(Channel channel : channelsToStop) { - mLog.debug("Stopping channel: " + channel.toString()); + mLog.info("Stopping channel: " + channel.toString()); stopProcessing(channel, true); } } @@ -438,17 +431,17 @@ public void shutdown() * Adds a message listener that will be added to all channels to receive * any messages. */ - public void addAudioPacketListener(Listener listener) + public void addAudioSegmentListener(Listener listener) { - mAudioPacketListeners.add(listener); + mAudioSegmentListeners.add(listener); } /** * Removes a message listener. */ - public void removeAudioPacketListener(Listener listener) + public void removeAudioSegmentListener(Listener listener) { - mAudioPacketListeners.remove(listener); + mAudioSegmentListeners.remove(listener); } /** diff --git a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java index e655c8d2b..2502b4d94 100644 --- a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java +++ b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java @@ -47,7 +47,7 @@ public JavaFxWindowManager(UserPreferences userPreferences) } /** - * Closes all JavaFX windows and shutsdown the FX thread + * Closes all JavaFX windows and shuts down the FX thread */ public void shutdown() { @@ -84,6 +84,8 @@ public void process(final PreferenceEditorViewRequest request) { Stage stage = mPreferencesEditor.getStage(); stage.show(); + stage.toFront(); + stage.requestFocus(); mPreferencesEditor.showEditor(request); } catch(Throwable t) diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 7711d59f0..7c2eb661b 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -1,30 +1,28 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * ***************************************************************************** + * 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.gui; import com.jidesoft.plaf.LookAndFeelFactory; import com.jidesoft.swing.JideSplitPane; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.AudioPacketManager; +import io.github.dsheirer.audio.broadcast.AudioStreamingManager; +import io.github.dsheirer.audio.broadcast.BroadcastFormat; import io.github.dsheirer.audio.broadcast.BroadcastModel; import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel; import io.github.dsheirer.audio.playback.AudioPlaybackManager; @@ -45,7 +43,7 @@ import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.properties.SystemProperties; -import io.github.dsheirer.record.RecorderManager; +import io.github.dsheirer.record.AudioRecordingManager; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.SourceManager; @@ -60,6 +58,7 @@ import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.javafx.IconFontFX; import jiconfont.swing.IconFontSwing; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -108,7 +107,8 @@ public class SDRTrunk implements Listener private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame"; private boolean mBroadcastStatusVisible; - private AudioPacketManager mAudioPacketManager; + private AudioRecordingManager mAudioRecordingManager; + private AudioStreamingManager mAudioStreamingManager; private IconManager mIconManager; private BroadcastStatusPanel mBroadcastStatusPanel; private BroadcastModel mBroadcastModel; @@ -164,6 +164,7 @@ public SDRTrunk() //Register FontAwesome so we can use the fonts in Swing windows IconFontSwing.register(FontAwesome.getIconFont()); + IconFontFX.register(FontAwesome.getIconFont()); TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel(); TunerModel tunerModel = new TunerModel(tunerConfigurationModel); @@ -180,14 +181,10 @@ public SDRTrunk() EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences); - RecorderManager recorderManager = new RecorderManager(aliasModel, mUserPreferences); - mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences); - mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences); - - mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, recorderManager, - mSourceManager, aliasModel, mUserPreferences); + mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, mSourceManager, + aliasModel, mUserPreferences); mChannelModel.addListener(mChannelProcessingManager); mChannelProcessingManager.addChannelEventListener(mChannelModel); @@ -195,25 +192,26 @@ public SDRTrunk() ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager(mChannelModel); mChannelModel.addListener(channelSelectionManager); - AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mSourceManager.getMixerManager()); + AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mUserPreferences); + + mAudioRecordingManager = new AudioRecordingManager(mUserPreferences); + mAudioRecordingManager.start(); mBroadcastModel = new BroadcastModel(aliasModel, mIconManager, mUserPreferences); + mAudioStreamingManager = new AudioStreamingManager(mBroadcastModel, BroadcastFormat.MP3, + mUserPreferences); + mAudioStreamingManager.start(); - //Audio packets are routed through the audio packet manager for metadata enrichment and then - //distributed to the audio packet processors (ie playback, recording, streaming, etc.) - mAudioPacketManager = new AudioPacketManager(aliasModel); - mAudioPacketManager.addListener(recorderManager); - mAudioPacketManager.addListener(audioPlaybackManager); - mAudioPacketManager.addListener(mBroadcastModel); - mAudioPacketManager.start(); - mChannelProcessingManager.addAudioPacketListener(mAudioPacketManager); + mChannelProcessingManager.addAudioSegmentListener(audioPlaybackManager); + mChannelProcessingManager.addAudioSegmentListener(mAudioRecordingManager); + mChannelProcessingManager.addAudioSegmentListener(mAudioStreamingManager); MapService mapService = new MapService(mIconManager); mChannelProcessingManager.addDecodeEventListener(mapService); mControllerPanel = new ControllerPanel(audioPlaybackManager, aliasModel, mBroadcastModel, mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager, - mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences, recorderManager); + mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences); mSpectralPanel = new SpectralDisplayPanel(mChannelModel, mChannelProcessingManager, mSettingsManager, tunerModel); @@ -411,7 +409,7 @@ public void actionPerformed(ActionEvent event) viewMenu.add(new JSeparator()); JMenuItem preferencesItem = new JMenuItem("Preferences"); preferencesItem.addActionListener(e -> MyEventBus.getEventBus() - .post(new PreferenceEditorViewRequest(PreferenceEditorType.CHANNEL_EVENT))); + .post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK))); viewMenu.add(preferencesItem); menuBar.add(viewMenu); @@ -471,7 +469,8 @@ private void processShutdown() mJavaFxWindowManager.shutdown(); mLog.info("Stopping channels ..."); mChannelProcessingManager.shutdown(); - mAudioPacketManager.stop(); + mAudioRecordingManager.stop(); + mLog.info("Stopping spectral display ..."); mSpectralPanel.clearTuner(); mSourceManager.shutdown(); diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java index 4256d5f44..3db092159 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,13 +14,15 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.gui.preference; import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor; import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor; +import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor; +import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor; import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor; import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor; import io.github.dsheirer.preference.UserPreferences; @@ -36,18 +37,22 @@ public static Node getEditor(PreferenceEditorType preferenceEditorType, UserPref { switch(preferenceEditorType) { + case AUDIO_PLAYBACK: + return new PlaybackPreferenceEditor(userPreferences); + case AUDIO_RECORD: + return new RecordPreferenceEditor(userPreferences); case CHANNEL_EVENT: return new DecodeEventViewPreferenceEditor(userPreferences); - case JMBE_LIBRARY: - return new JmbeLibraryPreferenceEditor(userPreferences); case DIRECTORY: return new DirectoryPreferenceEditor(userPreferences); + case JMBE_LIBRARY: + return new JmbeLibraryPreferenceEditor(userPreferences); case SOURCE_CHANNEL_MULTIPLE_FREQUENCY: return new ChannelMultipleFrequencyPreferenceEditor(userPreferences); - case TALKGROUP_FORMAT: - return new TalkgroupFormatPreferenceEditor(userPreferences); case SOURCE_TUNER_CHANNELIZER: return new TunerPreferenceEditor(userPreferences); + case TALKGROUP_FORMAT: + return new TalkgroupFormatPreferenceEditor(userPreferences); } return null; diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java index fd7ecce97..d272f4923 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.gui.preference; @@ -28,11 +25,14 @@ public enum PreferenceEditorType { CHANNEL_EVENT("Channel Events"), - JMBE_LIBRARY("JMBE Audio Library"), DIRECTORY("Directories"), + JMBE_LIBRARY("JMBE Audio Library"), + AUDIO_RECORD("Record"), + AUDIO_PLAYBACK("Playback"), SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"), SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"), - TALKGROUP_FORMAT("Talkgroup & Radio ID"); + TALKGROUP_FORMAT("Talkgroup & Radio ID"), + DEFAULT("Default"); private String mLabel; diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java b/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java index 2e9c08b50..62132eb32 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/PreferencesEditor.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,7 +14,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.gui.preference; @@ -29,12 +28,13 @@ import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.Control; import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.stage.Window; @@ -52,25 +52,19 @@ public class PreferencesEditor extends Application { private final static Logger mLog = LoggerFactory.getLogger(PreferencesEditor.class); + private Map mEditors = new HashMap<>(); private UserPreferences mUserPreferences; - private BorderPane mBorderPane; + private HBox mParentBox; private TreeView mEditorSelectionTreeView; - private DecodeEventViewPreferenceEditor mDecodeEventViewPreferenceEditor; - private VBox mDefaultEditor; - private HBox mControlBox; - - /** - * Constructs a preferences editor instance - */ - public PreferencesEditor() - { - } + private VBox mEditorAndButtonsBox; + private Node mEditor; + private HBox mButtonsBox; public Stage getStage() { try { - Window window = getBorderPane().getScene().getWindow(); + Window window = getParentBox().getScene().getWindow(); if(window instanceof Stage) { @@ -104,7 +98,7 @@ private UserPreferences getUserPreferences() public void start(Stage stage) throws Exception { stage.setTitle("Preferences"); - Scene scene = new Scene(getBorderPane(), 900, 500); + Scene scene = new Scene(getParentBox(), 900, 500); stage.setScene(scene); stage.show(); } @@ -130,8 +124,7 @@ private TreeItem recursivelyFindEditorType(TreeItem parent, PreferenceEditorType { TreeItem treeItem = li.next(); - if(treeItem.getValue() instanceof PreferenceEditorType && - ((PreferenceEditorType)treeItem.getValue()).equals(type)) + if(treeItem.getValue() instanceof PreferenceEditorType && (treeItem.getValue()).equals(type)) { return treeItem; } @@ -153,31 +146,48 @@ private TreeItem recursivelyFindEditorType(TreeItem parent, PreferenceEditorType /** * Primary layout for the editor window */ - private BorderPane getBorderPane() + private HBox getParentBox() { - if(mBorderPane == null) + if(mParentBox == null) { - mBorderPane = new BorderPane(); - mBorderPane.setLeft(getEditorSelectionTreeView()); - mBorderPane.setCenter(getDefaultEditor()); - mBorderPane.setBottom(getControlBox()); + mParentBox = new HBox(); + mParentBox.getChildren().add(getEditorSelectionTreeView()); + HBox.setHgrow(getEditorAndButtonsBox(), Priority.ALWAYS); + mParentBox.getChildren().add(getEditorAndButtonsBox()); } - return mBorderPane; + return mParentBox; } - private VBox getDefaultEditor() + private VBox getEditorAndButtonsBox() { - if(mDefaultEditor == null) + if(mEditorAndButtonsBox == null) { - mDefaultEditor = new VBox(); - mDefaultEditor.setPadding(new Insets(10, 10, 10, 10)); - Label label = new Label("Please select a preference ..."); + mEditorAndButtonsBox = new VBox(); + mEditor = getDefaultEditor(); + VBox.setVgrow(getDefaultEditor(), Priority.ALWAYS); + VBox.setVgrow(getButtonsBox(), Priority.NEVER); + mEditorAndButtonsBox.getChildren().addAll(getDefaultEditor(), getButtonsBox()); + } + + return mEditorAndButtonsBox; + } - mDefaultEditor.getChildren().add(label); + private Node getDefaultEditor() + { + Node editor = mEditors.get(PreferenceEditorType.DEFAULT); + + if(editor == null) + { + VBox defaultEditor = new VBox(); + defaultEditor.setPadding(new Insets(10, 10, 10, 10)); + Label label = new Label("Please select a preference ..."); + defaultEditor.getChildren().add(label); + mEditors.put(PreferenceEditorType.DEFAULT, defaultEditor); + editor = defaultEditor; } - return mDefaultEditor; + return editor; } /** @@ -189,6 +199,12 @@ private TreeView getEditorSelectionTreeView() { TreeItem treeRoot = new TreeItem<>("Root node"); + TreeItem audioItem = new TreeItem<>("Audio"); + audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_PLAYBACK)); + audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD)); + treeRoot.getChildren().add(audioItem); + audioItem.setExpanded(true); + TreeItem decoderItem = new TreeItem<>("Decoder"); decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY)); treeRoot.getChildren().add(decoderItem); @@ -217,43 +233,56 @@ private TreeView getEditorSelectionTreeView() treeRoot.setExpanded(true); mEditorSelectionTreeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); mEditorSelectionTreeView.getSelectionModel().selectedItemProperty().addListener(new EditorTreeSelectionListener()); - } - return mEditorSelectionTreeView; - } - - /** - * Decode event view preferences editor - */ - private DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor() - { - if(mDecodeEventViewPreferenceEditor == null) - { - mDecodeEventViewPreferenceEditor = new DecodeEventViewPreferenceEditor(getUserPreferences()); + mEditorSelectionTreeView.setMinWidth(Control.USE_PREF_SIZE); } - return mDecodeEventViewPreferenceEditor; + return mEditorSelectionTreeView; } /** * Control box with OK button. */ - private HBox getControlBox() + private HBox getButtonsBox() { - if(mControlBox == null) + if(mButtonsBox == null) { - mControlBox = new HBox(); + mButtonsBox = new HBox(); + mButtonsBox.setMaxWidth(Double.MAX_VALUE); Button okButton = new Button("Ok"); okButton.setOnAction(event -> { - Stage stage = (Stage)getBorderPane().getScene().getWindow(); + Stage stage = (Stage)getParentBox().getScene().getWindow(); stage.close(); }); HBox.setMargin(okButton, new Insets(5, 5, 5, 5)); - mControlBox.setAlignment(Pos.CENTER_RIGHT); - mControlBox.getChildren().add(okButton); + mButtonsBox.setAlignment(Pos.CENTER_RIGHT); + mButtonsBox.getChildren().add(okButton); + } + + return mButtonsBox; + } + + private void setEditor(PreferenceEditorType type) + { + Node editor = mEditors.get(type); + + if(editor == null) + { + if(type == PreferenceEditorType.DEFAULT) + { + editor = getDefaultEditor(); + } + else + { + editor = PreferenceEditorFactory.getEditor(type, getUserPreferences()); + mEditors.put(type, editor); + } } - return mControlBox; + getEditorAndButtonsBox().getChildren().remove(mEditor); + VBox.setVgrow(editor, Priority.ALWAYS); + mEditor = editor; + getEditorAndButtonsBox().getChildren().add(0, mEditor); } /** @@ -263,44 +292,22 @@ private HBox getControlBox() */ public class EditorTreeSelectionListener implements ChangeListener { - private Map mEditors = new HashMap<>(); @Override public void changed(ObservableValue observable, Object oldValue, Object newValue) { - Node editor = getEditor(newValue); - BorderPane.setAlignment(editor, Pos.CENTER_LEFT); - getBorderPane().setCenter(editor); - } - - private Node getEditor(Object treeNodeItem) - { - if(treeNodeItem instanceof TreeItem) + if(newValue instanceof TreeItem) { - Object value = ((TreeItem)treeNodeItem).getValue(); + Object value = ((TreeItem)newValue).getValue(); if(value instanceof PreferenceEditorType) { - PreferenceEditorType type = (PreferenceEditorType)value; - - Node editor = mEditors.get(type); - - if(editor != null) - { - return editor; - } - - editor = PreferenceEditorFactory.getEditor(type, getUserPreferences()); - - if(editor != null) - { - mEditors.put(type, editor); - return editor; - } + setEditor((PreferenceEditorType)value); + return; } } - return getDefaultEditor(); + setEditor(PreferenceEditorType.DEFAULT); } } diff --git a/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java new file mode 100644 index 000000000..21948030d --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java @@ -0,0 +1,343 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.gui.preference.playback; + +import com.google.common.eventbus.Subscribe; +import io.github.dsheirer.audio.AudioFormats; +import io.github.dsheirer.eventbus.MyEventBus; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.preference.playback.PlaybackPreference; +import io.github.dsheirer.source.mixer.MixerChannelConfiguration; +import io.github.dsheirer.source.mixer.MixerManager; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.paint.Color; +import jiconfont.icons.font_awesome.FontAwesome; +import jiconfont.javafx.IconNode; +import org.controlsfx.control.ToggleSwitch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.DataLine; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + + +/** + * Preference settings for audio playback + */ +public class PlaybackPreferenceEditor extends HBox +{ + private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreferenceEditor.class); + private PlaybackPreference mPlaybackPreference; + private GridPane mEditorPane; + private ComboBox mMixerComboBox; + private Button mMixerTestButton; + private ToggleSwitch mUseAudioSegmentStartToneSwitch; + private Button mTestStartToneButton; + private ToggleSwitch mUseAudioSegmentPreemptToneSwitch; + private Button mTestPreemptToneButton; + private ComboBox mStartToneFrequencyComboBox; + private ComboBox mStartToneVolumeComboBox; + private ComboBox mPreemptToneFrequencyComboBox; + private ComboBox mPreemptToneVolumeComboBox; + + public PlaybackPreferenceEditor(UserPreferences userPreferences) + { + mPlaybackPreference = userPreferences.getPlaybackPreference(); + + HBox.setHgrow(getEditorPane(), Priority.ALWAYS); + getChildren().add(getEditorPane()); + } + + private GridPane getEditorPane() + { + if(mEditorPane == null) + { + int row = 0; + mEditorPane = new GridPane(); + mEditorPane.setPadding(new Insets(10, 10, 10, 10)); + mEditorPane.setHgap(10); + mEditorPane.setVgap(10); + Label outputLabel = new Label("Audio Output Device"); + GridPane.setHalignment(outputLabel, HPos.RIGHT); + mEditorPane.add(outputLabel, 0, row, 2, 1); + mEditorPane.add(getMixerComboBox(), 2, row, 3, 1); + mEditorPane.add(getMixerTestButton(), 5, row); + mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, ++row, 6, 1); + mEditorPane.add(new Label("Audio Playback Insert Tones"), 0, ++row, 2, 1); + mEditorPane.add(getUseAudioSegmentStartToneSwitch(), 0, ++row); + mEditorPane.add(new Label("Start Tone"), 1, row, 3, 1); + Label startFrequencyLabel = new Label("Frequency:"); + GridPane.setHalignment(startFrequencyLabel, HPos.RIGHT); + mEditorPane.add(startFrequencyLabel, 1, ++row); + mEditorPane.add(getStartToneFrequencyComboBox(), 2, row); + Label startVolumeLabel = new Label("Volume:"); + GridPane.setHalignment(startVolumeLabel, HPos.RIGHT); + mEditorPane.add(startVolumeLabel, 3, row); + mEditorPane.add(getStartToneVolumeComboBox(), 4, row); + mEditorPane.add(getTestStartToneButton(), 5, row); + mEditorPane.add(getUseAudioSegmentPreemptToneSwitch(), 0, ++row); + mEditorPane.add(new Label("Priority Preempt Tone"), 1, row, 3, 1); + Label preemptFrequencyLabel = new Label("Frequency:"); + GridPane.setHalignment(preemptFrequencyLabel, HPos.RIGHT); + mEditorPane.add(preemptFrequencyLabel, 1, ++row); + mEditorPane.add(getPreemptToneFrequencyComboBox(), 2, row); + Label preemptVolumeLabel = new Label("Volume:"); + GridPane.setHalignment(preemptVolumeLabel, HPos.RIGHT); + mEditorPane.add(preemptVolumeLabel, 3, row); + mEditorPane.add(getPreemptToneVolumeComboBox(), 4, row); + mEditorPane.add(getTestPreemptToneButton(), 5, row); + } + + return mEditorPane; + } + + private ComboBox getMixerComboBox() + { + if(mMixerComboBox == null) + { + mMixerComboBox = new ComboBox<>(); + mMixerComboBox.getItems().addAll(MixerManager.getOutputMixers()); + mMixerComboBox.getSelectionModel().select(mPlaybackPreference.getMixerChannelConfiguration()); + mMixerComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setMixerChannelConfiguration(newValue)); + } + + return mMixerComboBox; + } + + public Button getMixerTestButton() + { + if(mMixerTestButton == null) + { + mMixerTestButton = new Button("Test"); + IconNode iconNode = new IconNode(FontAwesome.PLAY); + iconNode.setFill(Color.CORNFLOWERBLUE); + mMixerTestButton.setGraphic(iconNode); + mMixerTestButton.setOnAction(event -> play(mPlaybackPreference.getMixerTestTone())); + } + + return mMixerTestButton; + } + + private ToggleSwitch getUseAudioSegmentStartToneSwitch() + { + if(mUseAudioSegmentStartToneSwitch == null) + { + mUseAudioSegmentStartToneSwitch = new ToggleSwitch(); + mUseAudioSegmentStartToneSwitch.setAlignment(Pos.BASELINE_RIGHT); + mUseAudioSegmentStartToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentStartTone()); + mUseAudioSegmentStartToneSwitch.selectedProperty().addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) + { + mPlaybackPreference.setUseAudioSegmentStartTone(newValue); + } + }); + } + + return mUseAudioSegmentStartToneSwitch; + } + + private ToggleSwitch getUseAudioSegmentPreemptToneSwitch() + { + if(mUseAudioSegmentPreemptToneSwitch == null) + { + mUseAudioSegmentPreemptToneSwitch = new ToggleSwitch(); + mUseAudioSegmentPreemptToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentPreemptTone()); + mUseAudioSegmentPreemptToneSwitch.selectedProperty().addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) + { + mPlaybackPreference.setUseAudioSegmentPreemptTone(newValue); + } + }); + } + + return mUseAudioSegmentPreemptToneSwitch; + } + + public Button getTestStartToneButton() + { + if(mTestStartToneButton == null) + { + mTestStartToneButton = new Button("Test"); + IconNode iconNode = new IconNode(FontAwesome.PLAY); + iconNode.setFill(Color.CORNFLOWERBLUE); + mTestStartToneButton.setGraphic(iconNode); + mTestStartToneButton.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + play(mPlaybackPreference.getStartTone()); + } + }); + + mTestStartToneButton.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not()); + } + + return mTestStartToneButton; + } + + public Button getTestPreemptToneButton() + { + if(mTestPreemptToneButton == null) + { + mTestPreemptToneButton = new Button("Test"); + IconNode iconNode = new IconNode(FontAwesome.PLAY); + iconNode.setFill(Color.CORNFLOWERBLUE); + mTestPreemptToneButton.setGraphic(iconNode); + mTestPreemptToneButton.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + play(mPlaybackPreference.getPreemptTone()); + } + }); + + mTestPreemptToneButton.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not()); + } + + return mTestPreemptToneButton; + } + + public ComboBox getStartToneFrequencyComboBox() + { + if(mStartToneFrequencyComboBox == null) + { + mStartToneFrequencyComboBox = new ComboBox<>(); + mStartToneFrequencyComboBox.getItems().addAll(ToneFrequency.values()); + mStartToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getStartToneFrequency()); + mStartToneFrequencyComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setStartToneFrequency(newValue)); + mStartToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not()); + } + + return mStartToneFrequencyComboBox; + } + + public ComboBox getStartToneVolumeComboBox() + { + if(mStartToneVolumeComboBox == null) + { + mStartToneVolumeComboBox = new ComboBox<>(); + mStartToneVolumeComboBox.getItems().addAll(ToneVolume.values()); + mStartToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getStartToneVolume()); + mStartToneVolumeComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setStartToneVolume(newValue)); + mStartToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not()); + } + + return mStartToneVolumeComboBox; + } + + public ComboBox getPreemptToneFrequencyComboBox() + { + if(mPreemptToneFrequencyComboBox == null) + { + mPreemptToneFrequencyComboBox = new ComboBox<>(); + mPreemptToneFrequencyComboBox.getItems().addAll(ToneFrequency.values()); + mPreemptToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneFrequency()); + mPreemptToneFrequencyComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneFrequency(newValue)); + mPreemptToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not()); + } + + return mPreemptToneFrequencyComboBox; + } + + public ComboBox getPreemptToneVolumeComboBox() + { + if(mPreemptToneVolumeComboBox == null) + { + mPreemptToneVolumeComboBox = new ComboBox<>(); + mPreemptToneVolumeComboBox.getItems().addAll(ToneVolume.values()); + mPreemptToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneVolume()); + mPreemptToneVolumeComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneVolume(newValue)); + mPreemptToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not()); + } + + return mPreemptToneVolumeComboBox; + } + + /** + * Plays the audio buffer over the default mono playback device + * @param audioSamples with 8 kHz mono PCM samples + */ + private void play(float[] audioSamples) + { + if(audioSamples != null) + { + /* Little-endian byte buffer */ + ByteBuffer buffer = ByteBuffer.allocate(audioSamples.length * 2).order(ByteOrder.LITTLE_ENDIAN); + + ShortBuffer shortBuffer = buffer.asShortBuffer(); + + for(float sample : audioSamples) + { + shortBuffer.put((short) (sample * Short.MAX_VALUE)); + } + + byte[] bytes = buffer.array(); + + DataLine.Info info = new DataLine.Info(Clip.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); + + if(!AudioSystem.isLineSupported(info)) + { + mLog.error("Audio clip playback is not supported on this system"); + return; + } + + try + { + Clip clip = (Clip)AudioSystem.getLine(info); + clip.open(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, bytes, 0, bytes.length); + clip.start(); + } + catch(Exception e) + { + mLog.error("Error attempting to play audio test tone"); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/gui/preference/playback/ToneFrequency.java b/src/main/java/io/github/dsheirer/gui/preference/playback/ToneFrequency.java new file mode 100644 index 000000000..e2dbb3f7d --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/playback/ToneFrequency.java @@ -0,0 +1,78 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.gui.preference.playback; + +public enum ToneFrequency +{ + F300(300), + F400(400), + F500(500), + F600(600), + F700(700), + F800(800), + F900(900), + F1000(1000), + F1100(1100), + F1200(1200), + F1300(1300), + F1400(1400), + F1500(1500), + F1600(1600), + F1700(1700), + F1800(1800), + F1900(1900), + F2000(2000), + F2100(2100), + F2200(2200), + F2300(2300), + F2400(2400), + F2500(2500); + + private int mValue; + + ToneFrequency(int value) + { + mValue = value; + } + + public int getValue() + { + return mValue; + } + + @Override + public String toString() + { + return String.valueOf(mValue); + } + + public static ToneFrequency fromValue(int value) + { + for(ToneFrequency toneFrequency: ToneFrequency.values()) + { + if(toneFrequency.getValue() == value) + { + return toneFrequency; + } + } + + return F1200; //default + } +} diff --git a/src/main/java/io/github/dsheirer/gui/preference/playback/ToneUtil.java b/src/main/java/io/github/dsheirer/gui/preference/playback/ToneUtil.java new file mode 100644 index 000000000..868277b1f --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/playback/ToneUtil.java @@ -0,0 +1,68 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.gui.preference.playback; + +import io.github.dsheirer.dsp.mixer.Oscillator; + +/** + * Utility for generating tones for audio clips + */ +public class ToneUtil +{ + //Maximum tone volume: 0.0 <> 1.0 + private static final double MAX_TONE_VOLUME = 0.5f; + + /** + * Generates a tone using the specified parameters + * @param toneFrequency of the tone + * @param toneVolume of the tone 1-10, to a maximum 90% gain + * @param sampleCount number of 8 kHz samples to generate + * @return buffer of audio + */ + public static float[] getTone(ToneFrequency toneFrequency, ToneVolume toneVolume, int sampleCount) + { + Oscillator oscillator = new Oscillator(toneFrequency.getValue(), 8000.0); + + float[] samples = oscillator.generateReal(sampleCount); + + double gain = MAX_TONE_VOLUME * ((double)toneVolume.getValue() / 10.0); + + for(int x = 0; x < samples.length; x++) + { + samples[x] *= gain; + } + + //Attenuate beginning and end samples + if(sampleCount > 10) + { + for(int x = 0; x < 10; x++) + { + samples[x] *= (float)x / 10.0f; + } + + for(int x = 0; x < 10; x++) + { + samples[samples.length - 1 - x] *= (float)x / 10.0f; + } + } + + return samples; + } +} diff --git a/src/main/java/io/github/dsheirer/gui/preference/playback/ToneVolume.java b/src/main/java/io/github/dsheirer/gui/preference/playback/ToneVolume.java new file mode 100644 index 000000000..9337604ce --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/playback/ToneVolume.java @@ -0,0 +1,65 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.gui.preference.playback; + +public enum ToneVolume +{ + V1(1), + V2(2), + V3(3), + V4(4), + V5(5), + V6(6), + V7(7), + V8(8), + V9(9), + V10(10); + + private int mValue; + + ToneVolume(int value) + { + mValue = value; + } + + public int getValue() + { + return mValue; + } + + @Override + public String toString() + { + return String.valueOf(mValue); + } + + public static ToneVolume fromValue(int value) + { + for(ToneVolume toneVolume: ToneVolume.values()) + { + if(toneVolume.getValue() == value) + { + return toneVolume; + } + } + + return V3; //default + } +} diff --git a/src/main/java/io/github/dsheirer/gui/preference/record/RecordPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/record/RecordPreferenceEditor.java new file mode 100644 index 000000000..4f11862d3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/preference/record/RecordPreferenceEditor.java @@ -0,0 +1,91 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.gui.preference.record; + +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.preference.record.RecordPreference; +import io.github.dsheirer.record.RecordFormat; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Preference settings for recording + */ +public class RecordPreferenceEditor extends HBox +{ + private final static Logger mLog = LoggerFactory.getLogger(RecordPreferenceEditor.class); + private RecordPreference mRecordPreference; + private GridPane mEditorPane; + private ComboBox mRecordFormatComboBox; + + public RecordPreferenceEditor(UserPreferences userPreferences) + { + mRecordPreference = userPreferences.getRecordPreference(); + HBox.setHgrow(getEditorPane(), Priority.ALWAYS); + getChildren().add(getEditorPane()); + } + + private GridPane getEditorPane() + { + if(mEditorPane == null) + { + mEditorPane = new GridPane(); + mEditorPane.setPadding(new Insets(10, 10, 10, 10)); + mEditorPane.setHgap(10); + mEditorPane.setVgap(10); + + Label label = new Label("Audio Recording Format:"); + mEditorPane.add(label, 0, 0); + + mEditorPane.add(getRecordFormatComboBox(), 1, 0); + } + + return mEditorPane; + } + + private ComboBox getRecordFormatComboBox() + { + if(mRecordFormatComboBox == null) + { + mRecordFormatComboBox = new ComboBox<>(); + mRecordFormatComboBox.getItems().addAll(RecordFormat.values()); + mRecordFormatComboBox.getSelectionModel().select(mRecordPreference.getAudioRecordFormat()); + mRecordFormatComboBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, RecordFormat oldValue, RecordFormat newValue) + { + mRecordPreference.setAudioRecordFormat(newValue); + } + }); + } + + return mRecordFormatComboBox; + } +} diff --git a/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java index 184ff3437..8a290abf6 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/tuner/TunerPreferenceEditor.java @@ -64,11 +64,11 @@ private GridPane getEditorPane() if(mEditorPane == null) { mEditorPane = new GridPane(); + mEditorPane.setVgap(10); + mEditorPane.setHgap(10); mEditorPane.setPadding(new Insets(10, 10, 10, 10)); - GridPane.setMargin(getChannelizerLabel(), new Insets(0, 10, 0, 0)); GridPane.setHalignment(getChannelizerLabel(), HPos.LEFT); mEditorPane.add(getChannelizerLabel(), 0, 0); - GridPane.setMargin(getChannelizerTypeChoiceBox(), new Insets(2, 0, 2, 0)); mEditorPane.add(getChannelizerTypeChoiceBox(), 1, 0); mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, 1, 2, 1); mEditorPane.add(getPolyphaseLabel(), 0, 2, 2, 1); diff --git a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java index 82ae27108..673046eda 100644 --- a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java +++ b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.identifier; @@ -41,7 +38,6 @@ public class IdentifierCollection private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class); protected List mIdentifiers = new ArrayList<>(); protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier; - private boolean mUpdated = false; private int mTimeslot = 0; /** @@ -94,19 +90,6 @@ public void setTimeslot(int timeslot) mTimeslot = timeslot; } - /** - * Indicates if this identifier collection contains an updated list of identifiers. - */ - public boolean isUpdated() - { - return mUpdated; - } - - public void setUpdated(boolean updated) - { - mUpdated = updated; - } - /** * Alias List configuration identifier containing the name of the alias list for this collection. * @return alias list or null @@ -319,4 +302,19 @@ public Identifier getToIdentifier() return null; } + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Identifier Collection - Timeslot:").append(mTimeslot).append("\n"); + for(Identifier identifier: getIdentifiers()) + { + sb.append("\t").append(identifier.toString()); + sb.append("\t{").append(identifier.getIdentifierClass().name()).append("|") + .append(identifier.getForm().name()).append("|") + .append(identifier.getRole().name()).append("}"); + sb.append("\t").append(identifier.getClass()).append("\n"); + } + return sb.toString(); + } } diff --git a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java index 04e1ab700..211cd4dad 100644 --- a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java +++ b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java @@ -98,7 +98,6 @@ public void removeIdentifierUpdateListener() */ private void notifyAdd(Identifier identifier) { - setUpdated(true); if(mListener != null) { mListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot())); @@ -110,7 +109,6 @@ private void notifyAdd(Identifier identifier) */ private void notifyRemove(Identifier identifier) { - setUpdated(true); if(mListener != null) { mListener.receive(new IdentifierUpdateNotification(identifier, @@ -409,8 +407,6 @@ else if(identifierUpdateNotification.isRemove() || identifierUpdateNotification. public IdentifierCollection copyOf() { IdentifierCollection copy = new IdentifierCollection(getIdentifiers(), getTimeslot()); - copy.setUpdated(isUpdated()); - setUpdated(false); return copy; } } diff --git a/src/main/java/io/github/dsheirer/identifier/string/SimpleStringIdentifier.java b/src/main/java/io/github/dsheirer/identifier/string/SimpleStringIdentifier.java new file mode 100644 index 000000000..9877e6900 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/string/SimpleStringIdentifier.java @@ -0,0 +1,42 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.identifier.string; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.protocol.Protocol; + +/** + * Simple string identifier + */ +public class SimpleStringIdentifier extends StringIdentifier +{ + public SimpleStringIdentifier(String value, IdentifierClass identifierClass, Form form, Role role) + { + super(value, identifierClass, form, role); + } + + @Override + public Protocol getProtocol() + { + return Protocol.UNKNOWN; + } +} diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java index ca757551a..efc8bc881 100644 --- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java +++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java @@ -1,29 +1,28 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * ***************************************************************************** + * 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.module; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.IAudioPacketListener; -import io.github.dsheirer.audio.IAudioPacketProvider; +import io.github.dsheirer.audio.AudioSegment; +import io.github.dsheirer.audio.AudioSegmentBroadcaster; +import io.github.dsheirer.audio.IAudioSegmentListener; +import io.github.dsheirer.audio.IAudioSegmentProvider; import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder; import io.github.dsheirer.audio.squelch.ISquelchStateListener; import io.github.dsheirer.audio.squelch.ISquelchStateProvider; @@ -60,7 +59,6 @@ import io.github.dsheirer.sample.buffer.IReusableByteBufferListener; import io.github.dsheirer.sample.buffer.IReusableByteBufferProvider; import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster; import io.github.dsheirer.sample.buffer.ReusableByteBuffer; import io.github.dsheirer.sample.buffer.ReusableComplexBuffer; @@ -103,10 +101,10 @@ public class ProcessingChain implements Listener { private final static Logger mLog = LoggerFactory.getLogger(ProcessingChain.class); - private ReusableBufferBroadcaster mAudioPacketBroadcaster = new ReusableBufferBroadcaster<>(); private ReusableBufferBroadcaster mDemodulatedAudioBufferBroadcaster = new ReusableBufferBroadcaster(); private ReusableBufferBroadcaster mBasebandComplexBufferBroadcaster = new ReusableBufferBroadcaster(); private ReusableBufferBroadcaster mDemodulatedBitstreamBufferBroadcaster = new ReusableBufferBroadcaster(); + private Broadcaster mAudioSegmentBroadcaster = new AudioSegmentBroadcaster<>(); private Broadcaster mDecodeEventBroadcaster = new Broadcaster<>(); private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); @@ -180,7 +178,7 @@ public void dispose() mModules.clear(); - mAudioPacketBroadcaster.dispose(); + mAudioSegmentBroadcaster.dispose(); mDecodeEventBroadcaster.dispose(); mChannelEventBroadcaster.dispose(); mBasebandComplexBufferBroadcaster.dispose(); @@ -198,14 +196,6 @@ public boolean isProcessing() return mRunning.get(); } - /** - * Indicates if this chain currently has a valid sample source. - */ - public boolean hasSource() - { - return mSource != null; - } - /** * Indicates if this chain's source is the same as the source argument */ @@ -318,9 +308,9 @@ private void registerListeners(Module module) mIdentifierUpdateNotificationBroadcaster.addListener(((IdentifierUpdateListener)module).getIdentifierUpdateListener()); } - if(module instanceof IAudioPacketListener) + if(module instanceof IAudioSegmentListener) { - mAudioPacketBroadcaster.addListener(((IAudioPacketListener)module).getAudioPacketListener()); + mAudioSegmentBroadcaster.addListener(((IAudioSegmentListener)module).getAudioSegmentListener()); } if(module instanceof IDecodeEventListener) @@ -390,9 +380,9 @@ private void unregisterListeners(Module module) mIdentifierUpdateNotificationBroadcaster.removeListener(((IdentifierUpdateListener)module).getIdentifierUpdateListener()); } - if(module instanceof IAudioPacketListener) + if(module instanceof IAudioSegmentListener) { - mAudioPacketBroadcaster.removeListener(((IAudioPacketListener)module).getAudioPacketListener()); + mAudioSegmentBroadcaster.removeListener(((IAudioSegmentListener)module).getAudioSegmentListener()); } if(module instanceof IDecodeEventListener) @@ -457,9 +447,9 @@ private void registerProviders(Module module) ((IdentifierUpdateProvider)module).setIdentifierUpdateListener(mIdentifierUpdateNotificationBroadcaster); } - if(module instanceof IAudioPacketProvider) + if(module instanceof IAudioSegmentProvider) { - ((IAudioPacketProvider)module).setAudioPacketListener(mAudioPacketBroadcaster); + ((IAudioSegmentProvider)module).setAudioSegmentListener(mAudioSegmentBroadcaster); } if(module instanceof IDecodeEventProvider) @@ -524,9 +514,9 @@ private void unregisterProviders(Module module) ((IdentifierUpdateProvider)module).removeIdentifierUpdateListener(); } - if(module instanceof IAudioPacketProvider) + if(module instanceof IAudioSegmentProvider) { - ((IAudioPacketProvider)module).setAudioPacketListener(null); + ((IAudioSegmentProvider)module).setAudioSegmentListener(null); } if(module instanceof IReusableByteBufferProvider) @@ -733,14 +723,14 @@ else if(module instanceof BinaryRecorder) /** * Adds the listener to receive audio packets from all modules. */ - public void addAudioPacketListener(Listener listener) + public void addAudioSegmentListener(Listener listener) { - mAudioPacketBroadcaster.addListener(listener); + mAudioSegmentBroadcaster.addListener(listener); } - public void removeAudioPacketListener(Listener listener) + public void removeAudioSegmentListener(Listener listener) { - mAudioPacketBroadcaster.removeListener(listener); + mAudioSegmentBroadcaster.removeListener(listener); } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index 1a4fe6f56..81408711b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.module.decode; @@ -135,16 +132,8 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch { List modules = new ArrayList(); - //Adds an alias action manager when there are alias actions specified - if(channel.getAliasListName() != null && !channel.getAliasListName().isEmpty()) - { - AliasList aliasList = aliasModel.getAliasList(channel.getAliasListName()); - - if(aliasList != null && aliasList.hasAliasActions()) - { - modules.add(new AliasActionManager(aliasList)); - } - } + AliasList aliasList = aliasModel.getAliasList(channel.getAliasListName()); + modules.add(new AliasActionManager(aliasList)); ChannelType channelType = channel.getChannelType(); @@ -157,12 +146,13 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch modules.add(new AMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName())); + AudioModule audioModuleAM = new AudioModule(aliasList); + modules.add(audioModuleAM); + //Check if the user wants all audio recorded .. if(((DecodeConfigAM)decodeConfig).getRecordAudio()) { - AudioModule audioModuleAM = new AudioModule(); audioModuleAM.setRecordAudio(true); - modules.add(audioModuleAM); } if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) @@ -173,7 +163,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch case NBFM: modules.add(new NBFMDecoder(decodeConfig)); modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName())); - AudioModule audioModuleFM = new AudioModule(); + AudioModule audioModuleFM = new AudioModule(aliasList); //Check if the user wants all audio recorded .. if(((DecodeConfigNBFM)decodeConfig).getRecordAudio()) @@ -190,7 +180,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch MessageDirection direction = ((DecodeConfigLTRStandard)decodeConfig).getMessageDirection(); modules.add(new LTRStandardDecoder(null, direction)); modules.add(new LTRStandardDecoderState()); - modules.add(new AudioModule()); + modules.add(new AudioModule(aliasList)); if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) { modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); @@ -199,7 +189,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch case LTR_NET: modules.add(new LTRNetDecoder((DecodeConfigLTRNet)decodeConfig)); modules.add(new LTRNetDecoderState()); - modules.add(new AudioModule()); + modules.add(new AudioModule(aliasList)); if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) { modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); @@ -210,7 +200,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName()); Sync sync = mptConfig.getSync(); modules.add(new MPT1327Decoder(sync)); - modules.add(new AudioModule()); + modules.add(new AudioModule(aliasList)); SourceType sourceType = channel.getSourceConfiguration().getSourceType(); if(sourceType == SourceType.TUNER || sourceType == SourceType.TUNER_MULTIPLE_FREQUENCIES) { @@ -242,7 +232,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch case PASSPORT: modules.add(new PassportDecoder(decodeConfig)); modules.add(new PassportDecoderState()); - modules.add(new AudioModule()); + modules.add(new AudioModule(aliasList)); if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) { modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); @@ -275,7 +265,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch modules.add(new P25P1DecoderState(channel)); } - modules.add(new P25P1AudioModule(userPreferences)); + modules.add(new P25P1AudioModule(userPreferences, aliasList)); //Add a channel rotation monitor when we have multiple control channel frequencies specified if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency && @@ -291,8 +281,8 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch modules.add(new P25P2DecoderState(channel, 0)); modules.add(new P25P2DecoderState(channel, 1)); - modules.add(new P25P2AudioModule(userPreferences, 0)); - modules.add(new P25P2AudioModule(userPreferences, 1)); + modules.add(new P25P2AudioModule(userPreferences, 0, aliasList)); + modules.add(new P25P2AudioModule(userPreferences, 1, aliasList)); break; default: throw new IllegalArgumentException("Unknown decoder type [" + decodeConfig.getDecoderType().toString() + "]"); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java index d7291aa74..41ab474cc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P1AudioModule.java @@ -1,26 +1,24 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.module.decode.p25.audio; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.audio.codec.mbe.ImbeAudioModule; import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.audio.squelch.SquelchStateEvent; @@ -32,7 +30,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDUMessage; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; public class P25P1AudioModule extends ImbeAudioModule { @@ -43,9 +40,15 @@ public class P25P1AudioModule extends ImbeAudioModule private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f); private LDU1Message mCachedLDU1Message = null; - public P25P1AudioModule(UserPreferences userPreferences) + public P25P1AudioModule(UserPreferences userPreferences, AliasList aliasList) { - super(userPreferences); + super(userPreferences, aliasList); + } + + @Override + protected int getTimeslot() + { + return 0; } @Override @@ -63,21 +66,6 @@ public void reset() @Override public void start() { - - } - - @Override - public void stop() - { - if(hasAudioPacketListener()) - { - ReusableAudioPacket endAudioPacket = getAudioPacketQueue().getEndAudioBuffer(); - endAudioPacket.resetAttributes(); - endAudioPacket.setAudioChannelId(getAudioChannelId()); - endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - endAudioPacket.incrementUserCount(); - getAudioPacketListener().receive(endAudioPacket); - } } /** @@ -89,7 +77,7 @@ public void stop() */ public void receive(IMessage message) { - if(hasAudioCodec() && hasAudioPacketListener()) + if(hasAudioCodec()) { if(mEncryptedCallStateEstablished) { @@ -139,16 +127,8 @@ private void processAudio(LDUMessage ldu) for(byte[] frame : ldu.getIMBEFrames()) { float[] audio = getAudioCodec().getAudio(frame); - audio = mGain.apply(audio); - - ReusableAudioPacket audioPacket = getAudioPacketQueue().getBuffer(audio.length); - audioPacket.resetAttributes(); - audioPacket.setAudioChannelId(getAudioChannelId()); - audioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - audioPacket.loadAudioFrom(audio); - - getAudioPacketListener().receive(audioPacket); + addAudio(audio); } } else @@ -169,16 +149,7 @@ public void receive(SquelchStateEvent event) { if(event.getSquelchState() == SquelchState.SQUELCH) { - if(hasAudioPacketListener()) - { - ReusableAudioPacket endAudioPacket = getAudioPacketQueue().getEndAudioBuffer(); - endAudioPacket.resetAttributes(); - endAudioPacket.setAudioChannelId(getAudioChannelId()); - endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - endAudioPacket.incrementUserCount(); - getAudioPacketListener().receive(endAudioPacket); - } - + closeAudioSegment(); mEncryptedCallStateEstablished = false; mEncryptedCall = false; mCachedLDU1Message = null; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java index ce70679a6..a969c3ae9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java @@ -1,27 +1,25 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.module.decode.p25.audio; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule; import io.github.dsheirer.audio.squelch.SquelchState; import io.github.dsheirer.audio.squelch.SquelchStateEvent; @@ -39,7 +37,6 @@ import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; import jmbe.iface.IAudioWithMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,14 +67,14 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat private boolean mEncryptedCallStateEstablished = false; private boolean mEncryptedCall = false; - public P25P2AudioModule(UserPreferences userPreferences, int timeslot) + public P25P2AudioModule(UserPreferences userPreferences, int timeslot, AliasList aliasList) { - super(userPreferences); + super(userPreferences, aliasList); mTimeslot = timeslot; getIdentifierCollection().setTimeslot(timeslot); } - private int getTimeslot() + protected int getTimeslot() { return mTimeslot; } @@ -104,12 +101,6 @@ public void start() reset(); } - @Override - public void stop() - { - - } - /** * Primary message processing method for processing voice timeslots and Push-To-Talk MAC messages * @@ -149,7 +140,7 @@ else if(message instanceof PushToTalk && message.isValid()) //There should not be any pending voice timeslots to process since the PTT message is the first in //the audio call sequence } - else if(message instanceof EncryptionSynchronizationSequence) + else if(message instanceof EncryptionSynchronizationSequence && message.isValid()) { mEncryptedCallStateEstablished = true; mEncryptedCall = ((EncryptionSynchronizationSequence)message).isEncrypted(); @@ -174,7 +165,7 @@ private void processPendingVoiceTimeslots() private void processAudio(List voiceFrames) { - if(hasAudioCodec() && hasAudioPacketListener()) + if(hasAudioCodec()) { for(BinaryMessage voiceFrame: voiceFrames) { @@ -182,13 +173,7 @@ private void processAudio(List voiceFrames) IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes); processMetadata(audioWithMetadata); - - ReusableAudioPacket audioPacket = getAudioPacketQueue().getBuffer(audioWithMetadata.getAudio().length); - audioPacket.resetAttributes(); - audioPacket.setAudioChannelId(getAudioChannelId()); - audioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - audioPacket.loadAudioFrom(audioWithMetadata.getAudio()); - getAudioPacketListener().receive(audioPacket); + addAudio(audioWithMetadata.getAudio()); } } } @@ -356,19 +341,13 @@ public class SquelchStateListener implements Listener @Override public void receive(SquelchStateEvent event) { - if(event.getTimeslot() == getTimeslot() && event.getSquelchState() == SquelchState.SQUELCH) + if(event.getTimeslot() == getTimeslot()) { - if(hasAudioPacketListener()) + if(event.getSquelchState() == SquelchState.SQUELCH) { - ReusableAudioPacket endAudioPacket = getAudioPacketQueue().getEndAudioBuffer(); - endAudioPacket.resetAttributes(); - endAudioPacket.setAudioChannelId(getAudioChannelId()); - endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf()); - endAudioPacket.incrementUserCount(); - getAudioPacketListener().receive(endAudioPacket); + closeAudioSegment(); + reset(); } - - reset(); } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java index fb9a4dbb4..0cf9d2c4e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java @@ -1,28 +1,24 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * ***************************************************************************** + * 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.module.decode.p25.phase2; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.AudioPacketManager; import io.github.dsheirer.audio.playback.AudioPlaybackManager; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.controller.channel.Channel; @@ -213,13 +209,13 @@ public static void main(String[] args) UserPreferences userPreferences = new UserPreferences(); AliasModel aliasModel = new AliasModel(); SourceManager sourceManager = new SourceManager(null, new SettingsManager(new TunerConfigurationModel()), userPreferences); - AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(sourceManager.getMixerManager()); + AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(userPreferences); //Audio packets are routed through the audio packet manager for metadata enrichment and then //distributed to the audio packet processors (ie playback, recording, streaming, etc.) - AudioPacketManager audioPacketManager = new AudioPacketManager(aliasModel); - audioPacketManager.addListener(audioPlaybackManager); - audioPacketManager.start(); +// AudioPacketManager audioPacketManager = new AudioPacketManager(aliasModel); +// audioPacketManager.addListener(audioPlaybackManager); +// audioPacketManager.start(); Channel channel = new Channel(); DecodeConfigP25Phase2 decodeP2 = new DecodeConfigP25Phase2(); @@ -230,7 +226,7 @@ public static void main(String[] args) MessageInjectionModule messageInjectionModule = new MessageInjectionModule(); modules.add(messageInjectionModule); ProcessingChain processingChain = new ProcessingChain(channel, aliasModel); - processingChain.addAudioPacketListener(audioPacketManager); +// processingChain.addAudioPacketListener(audioPacketManager); processingChain.addModules(modules); processingChain.start(); diff --git a/src/main/java/io/github/dsheirer/preference/PreferenceType.java b/src/main/java/io/github/dsheirer/preference/PreferenceType.java index c17a58856..b6446ad17 100644 --- a/src/main/java/io/github/dsheirer/preference/PreferenceType.java +++ b/src/main/java/io/github/dsheirer/preference/PreferenceType.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.preference; @@ -32,6 +29,8 @@ public enum PreferenceType IDENTIFIER, JMBE_LIBRARY, MULTI_FREQUENCY, + PLAYBACK, RADIO_REFERENCE, + RECORD, TUNER; } diff --git a/src/main/java/io/github/dsheirer/preference/UserPreferences.java b/src/main/java/io/github/dsheirer/preference/UserPreferences.java index 0682f3b77..f8cd9d1b4 100644 --- a/src/main/java/io/github/dsheirer/preference/UserPreferences.java +++ b/src/main/java/io/github/dsheirer/preference/UserPreferences.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.preference; @@ -27,12 +24,15 @@ import io.github.dsheirer.preference.directory.DirectoryPreference; import io.github.dsheirer.preference.event.DecodeEventPreference; import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference; +import io.github.dsheirer.preference.playback.PlaybackPreference; import io.github.dsheirer.preference.playlist.PlaylistPreference; import io.github.dsheirer.preference.radioreference.RadioReferencePreference; +import io.github.dsheirer.preference.record.RecordPreference; import io.github.dsheirer.preference.source.ChannelMultiFrequencyPreference; import io.github.dsheirer.preference.source.TunerPreference; import io.github.dsheirer.preference.swing.SwingPreference; import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.mixer.MixerManager; /** * User Preferences. A collection of preferences that can be accessed by preference type. @@ -56,8 +56,10 @@ public class UserPreferences implements Listener private DecodeEventPreference mDecodeEventPreference; private DirectoryPreference mDirectoryPreference; private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference; + private PlaybackPreference mPlaybackPreference; private PlaylistPreference mPlaylistPreference; private RadioReferencePreference mRadioReferencePreference; + private RecordPreference mRecordPreference; private TalkgroupFormatPreference mTalkgroupFormatPreference; private TunerPreference mTunerPreference; private SwingPreference mSwingPreference = new SwingPreference(); @@ -102,6 +104,14 @@ public ChannelMultiFrequencyPreference getChannelMultiFrequencyPreference() return mChannelMultiFrequencyPreference; } + /** + * Audio playback preferences + */ + public PlaybackPreference getPlaybackPreference() + { + return mPlaybackPreference; + } + /** * Playlist preferences */ @@ -118,6 +128,14 @@ public RadioReferencePreference getRadioReferencePreference() return mRadioReferencePreference; } + /** + * Recording preferences + */ + public RecordPreference getRecordPreference() + { + return mRecordPreference; + } + /** * Identifier preferences */ @@ -152,8 +170,10 @@ private void loadPreferenceTypes() mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive); mDirectoryPreference = new DirectoryPreference(this::receive); mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive); + mPlaybackPreference = new PlaybackPreference(this::receive); mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference); mRadioReferencePreference = new RadioReferencePreference(this::receive); + mRecordPreference = new RecordPreference(this::receive); mTalkgroupFormatPreference = new TalkgroupFormatPreference(this::receive); mTunerPreference = new TunerPreference(this::receive); } diff --git a/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java b/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java new file mode 100644 index 000000000..c17d4c2e0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java @@ -0,0 +1,288 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.preference.playback; + +import io.github.dsheirer.gui.preference.playback.ToneFrequency; +import io.github.dsheirer.gui.preference.playback.ToneUtil; +import io.github.dsheirer.gui.preference.playback.ToneVolume; +import io.github.dsheirer.preference.Preference; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.mixer.MixerChannel; +import io.github.dsheirer.source.mixer.MixerChannelConfiguration; +import io.github.dsheirer.source.mixer.MixerManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.prefs.Preferences; + +/** + * User preferences for audio playback + */ +public class PlaybackPreference extends Preference +{ + private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE = "audio.playback.segment.preempt.tone"; + private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE = "audio.playback.segment.start.tone"; + private static final String PREFERENCE_KEY_START_TONE_FREQUENCY = "audio.playback.segment.start.frequency"; + private static final String PREFERENCE_KEY_START_TONE_VOLUME = "audio.playback.segment.start.volume"; + private static final String PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY = "audio.playback.segment.preempt.frequency"; + private static final String PREFERENCE_KEY_PREEMPT_TONE_VOLUME = "audio.playback.segment.preempt.volume"; + private static final String PREFERENCE_KEY_MIXER_CHANNEL_CONFIG = "audio.playback.mixer.channel.configuration"; + private static final int TONE_LENGTH_SAMPLES = 180; + + private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreference.class); + private Preferences mPreferences = Preferences.userNodeForPackage(PlaybackPreference.class); + private Boolean mUseAudioSegmentStartTone; + private Boolean mUseAudioSegmentPreemptTone; + private ToneFrequency mStartToneFrequency; + private ToneVolume mStartToneVolume; + private ToneFrequency mPreemptToneFrequency; + private ToneVolume mPreemptToneVolume; + private MixerChannelConfiguration mMixerChannelConfiguration; + + /** + * Constructs this preference with an update listener + * @param updateListener to receive notifications whenever these preferences change + */ + public PlaybackPreference(Listener updateListener) + { + super(updateListener); + } + + @Override + public PreferenceType getPreferenceType() + { + return PreferenceType.PLAYBACK; + } + + /** + * Indicates if an audio segment start tone should be used. + */ + public boolean getUseAudioSegmentStartTone() + { + if(mUseAudioSegmentStartTone == null) + { + mUseAudioSegmentStartTone = mPreferences.getBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE, true); + } + + return mUseAudioSegmentStartTone; + } + + /** + * Sets the preference for using an audio segment start tone + */ + public void setUseAudioSegmentStartTone(boolean use) + { + mUseAudioSegmentStartTone = use; + mPreferences.putBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE, use); + notifyPreferenceUpdated(); + } + + /** + * Indicates if an audio segment preempt tone should be used. + */ + public boolean getUseAudioSegmentPreemptTone() + { + if(mUseAudioSegmentPreemptTone == null) + { + mUseAudioSegmentPreemptTone = mPreferences.getBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE, true); + } + + return mUseAudioSegmentPreemptTone; + } + + /** + * Sets the preference for using an audio segment preempt tone + */ + public void setUseAudioSegmentPreemptTone(boolean use) + { + mUseAudioSegmentPreemptTone = use; + mPreferences.putBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE, use); + notifyPreferenceUpdated(); + } + + /** + * Frequency for the start tone + */ + public ToneFrequency getStartToneFrequency() + { + if(mStartToneFrequency == null) + { + int frequency = mPreferences.getInt(PREFERENCE_KEY_START_TONE_FREQUENCY, ToneFrequency.F700.getValue()); + mStartToneFrequency = ToneFrequency.fromValue(frequency); + } + + return mStartToneFrequency; + } + + /** + * Sets the frequency for the start tone + */ + public void setStartToneFrequency(ToneFrequency toneFrequency) + { + mStartToneFrequency = toneFrequency; + mPreferences.putInt(PREFERENCE_KEY_START_TONE_FREQUENCY, toneFrequency.getValue()); + notifyPreferenceUpdated(); + } + + /** + * Frequency for the preempt tone + */ + public ToneFrequency getPreemptToneFrequency() + { + if(mPreemptToneFrequency == null) + { + int frequency = mPreferences.getInt(PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY, ToneFrequency.F400.getValue()); + mPreemptToneFrequency = ToneFrequency.fromValue(frequency); + } + + return mPreemptToneFrequency; + } + + /** + * Sets the frequency for the preempt tone + */ + public void setPreemptToneFrequency(ToneFrequency toneFrequency) + { + mPreemptToneFrequency = toneFrequency; + mPreferences.putInt(PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY, toneFrequency.getValue()); + notifyPreferenceUpdated(); + } + + /** + * Start tone volume + */ + public ToneVolume getStartToneVolume() + { + if(mStartToneVolume == null) + { + int volume = mPreferences.getInt(PREFERENCE_KEY_START_TONE_VOLUME, ToneVolume.V3.getValue()); + mStartToneVolume = ToneVolume.fromValue(volume); + } + + return mStartToneVolume; + } + + /** + * Sets the start tone volume + */ + public void setStartToneVolume(ToneVolume toneVolume) + { + mStartToneVolume = toneVolume; + mPreferences.putInt(PREFERENCE_KEY_START_TONE_VOLUME, toneVolume.getValue()); + notifyPreferenceUpdated(); + } + + /** + * Preempt tone volume + */ + public ToneVolume getPreemptToneVolume() + { + if(mPreemptToneVolume == null) + { + int volume = mPreferences.getInt(PREFERENCE_KEY_PREEMPT_TONE_VOLUME, ToneVolume.V5.getValue()); + mPreemptToneVolume = ToneVolume.fromValue(volume); + } + + return mPreemptToneVolume; + } + + /** + * Sets the preempt tone volume + */ + public void setPreemptToneVolume(ToneVolume toneVolume) + { + mPreemptToneVolume = toneVolume; + mPreferences.putInt(PREFERENCE_KEY_PREEMPT_TONE_VOLUME, toneVolume.getValue()); + notifyPreferenceUpdated(); + } + + /** + * Buffer with samples for the audio segment start tone + */ + public float[] getStartTone() + { + if(getUseAudioSegmentStartTone()) + { + return ToneUtil.getTone(getStartToneFrequency(), getStartToneVolume(), TONE_LENGTH_SAMPLES); + } + + return null; + } + + /** + * Buffer with samples for the audio segment preempt tone + */ + public float[] getPreemptTone() + { + if(getUseAudioSegmentPreemptTone()) + { + return ToneUtil.getTone(getPreemptToneFrequency(), getPreemptToneVolume(), TONE_LENGTH_SAMPLES); + } + + return null; + } + + /** + * Test tone to use for testing the currently selected mixer output + */ + public float[] getMixerTestTone() + { + return ToneUtil.getTone(ToneFrequency.F1200, ToneVolume.V10, 800); + } + + /** + * Gets the preferred output mixer to use + */ + public MixerChannelConfiguration getMixerChannelConfiguration() + { + if(mMixerChannelConfiguration == null) + { + MixerChannelConfiguration defaultConfig = MixerManager.getDefaultOutputMixer(); + + String configName = mPreferences.get(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, defaultConfig.toString()); + + for(MixerChannelConfiguration config: MixerManager.getOutputMixers()) + { + if(config.toString().contentEquals(configName)) + { + mMixerChannelConfiguration = config; + } + } + + if(mMixerChannelConfiguration == null) + { + mMixerChannelConfiguration = defaultConfig; + } + } + + return mMixerChannelConfiguration; + } + + /** + * Sets the preferred output mixer to use + */ + public void setMixerChannelConfiguration(MixerChannelConfiguration configuration) + { + mMixerChannelConfiguration = configuration; + mPreferences.put(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, configuration.toString()); + notifyPreferenceUpdated(); + } +} diff --git a/src/main/java/io/github/dsheirer/preference/record/RecordPreference.java b/src/main/java/io/github/dsheirer/preference/record/RecordPreference.java new file mode 100644 index 000000000..51cd2d701 --- /dev/null +++ b/src/main/java/io/github/dsheirer/preference/record/RecordPreference.java @@ -0,0 +1,93 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.preference.record; + +import io.github.dsheirer.preference.Preference; +import io.github.dsheirer.preference.PreferenceType; +import io.github.dsheirer.record.RecordFormat; +import io.github.dsheirer.sample.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.prefs.Preferences; + +/** + * User preferences for playlists + */ +public class RecordPreference extends Preference +{ + private static final String PREFERENCE_KEY_AUDIO_RECORD_FORMAT = "audio.record.format"; + private static final RecordFormat DEFAULT_RECORD_FORMAT = RecordFormat.MP3; + private final static Logger mLog = LoggerFactory.getLogger(RecordPreference.class); + private Preferences mPreferences = Preferences.userNodeForPackage(RecordPreference.class); + private RecordFormat mAudioRecordFormat; + + /** + * Constructs this preference with an update listener + * @param updateListener to receive notifications whenever these preferences change + */ + public RecordPreference(Listener updateListener) + { + super(updateListener); + } + + @Override + public PreferenceType getPreferenceType() + { + return PreferenceType.RECORD; + } + + + /** + * Audio recording format + */ + public RecordFormat getAudioRecordFormat() + { + if(mAudioRecordFormat == null) + { + try + { + String format = mPreferences.get(PREFERENCE_KEY_AUDIO_RECORD_FORMAT, DEFAULT_RECORD_FORMAT.name()); + mAudioRecordFormat = RecordFormat.valueOf(format); + } + catch(Exception e) + { + mLog.error("Error parsing record format preference", e); + } + + if(mAudioRecordFormat == null) + { + mAudioRecordFormat = DEFAULT_RECORD_FORMAT; + } + } + + return mAudioRecordFormat; + } + + /** + * Sets the audio recording format + */ + public void setAudioRecordFormat(RecordFormat audioRecordFormat) + { + mAudioRecordFormat = audioRecordFormat; + mPreferences.put(PREFERENCE_KEY_AUDIO_RECORD_FORMAT, audioRecordFormat.name()); + notifyPreferenceUpdated(); + } +} diff --git a/src/main/java/io/github/dsheirer/record/AudioRecorder.java b/src/main/java/io/github/dsheirer/record/AudioRecorder.java deleted file mode 100644 index b72f4f255..000000000 --- a/src/main/java/io/github/dsheirer/record/AudioRecorder.java +++ /dev/null @@ -1,340 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.record; - -import io.github.dsheirer.audio.IAudioPacketListener; -import io.github.dsheirer.identifier.IdentifierCollection; -import io.github.dsheirer.module.Module; -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; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class AudioRecorder extends Module implements Listener, IAudioPacketListener -{ - private final static Logger mLog = LoggerFactory.getLogger(AudioRecorder.class); - - private LinkedBlockingQueue mAudioPacketQueue = new LinkedBlockingQueue<>(500); - private List mPacketsToProcess = new ArrayList<>(); - - private FileOutputStream mFileOutputStream; - private AtomicBoolean mRunning = new AtomicBoolean(); - - protected Path mPath; - protected IdentifierCollection mIdentifierCollection; - protected long mTimeRecordingStart; - protected long mTimeLastPacketReceived; - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private Listener mRecordingClosedListener; - - private long mSampleCount; - - /** - * Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file - * - * @param path for the output recording - */ - public AudioRecorder(Path path) - { - mPath = path; - } - - /** - * Path for the audio recording file - */ - public Path getPath() - { - return mPath; - } - - /** - * Latest audio identifier collection received for this recording - */ - public IdentifierCollection getIdentifierCollection() - { - return mIdentifierCollection; - } - - /** - * Timestamp of the last buffer received by this recorder - allows this recorder to be monitored for automatic - * closure after a time period has elapsed. - */ - public long getTimeLastPacketReceived() - { - return mTimeLastPacketReceived; - } - - public long getTimeRecordingStart() - { - return mTimeRecordingStart; - } - - /** - * Recording length in milliseconds - */ - public long getRecordingLength() - { - //Assumes audio sample rate of 8000 samples/second or 8 samples/milli-second - return mSampleCount / 8; - } - - /** - * Implements the IAudioPacketListener interface and simply redirects to the Listener interface. - * This is necessary since you can't have multiple methods with the same erasure (ie Listener) in the - * parent module class. - */ - @Override - public Listener getAudioPacketListener() - { - return this; - } - - /** - * Processes the audio packet and captures the latest Metadata for the recording for easy access. - */ - @Override - public void receive(ReusableAudioPacket audioPacket) - { - if(mRunning.get()) - { - mTimeRecordingStart = System.currentTimeMillis(); - mTimeLastPacketReceived = mTimeRecordingStart; - - if(audioPacket.hasIdentifierCollection()) - { - mIdentifierCollection = audioPacket.getIdentifierCollection(); - } - - boolean success = mAudioPacketQueue.offer(audioPacket); - - if(!success) - { - mLog.error("recorder buffer overflow - stopping recorder [" + getPath().toString() + "]"); - stop(); - audioPacket.decrementUserCount(); - } - } - else - { - audioPacket.decrementUserCount(); - } - } - - /** - * File output stream for the current recording. Intended to allow sub-classes to write binary data to the file. - */ - protected OutputStream getOutputStream() - { - return mFileOutputStream; - } - - /** - * Stops the recorder and flags the recording to be closed. Use this method if you do not need any details about - * the final recording. Otherwise, use the close() method and register a closing listener. - */ - public void stop() - { - close(null); - } - - /** - * Closes the recording file. Upon successful closing of the recording file, the listener is notified that the - * audio recorder is closed. There is potential for the calling thread (here) and the buffer processor thread to - * both inform the recording closed listener that the recording is ended. So, we synchronize on the listener and - * the first thread to get the lock informs the listener and then nullifies the listener pointer so that if the - * second thread attempts a duplicate notification the listener would be null at that point. - */ - public void close(Listener listener) - { - mRecordingClosedListener = listener; - - if(!mRunning.compareAndSet(true, false)) - { - synchronized(mRecordingClosedListener) - { - if(mRecordingClosedListener != null) - { - mRecordingClosedListener.receive(AudioRecorder.this); - mRecordingClosedListener = null; - } - } - } - } - - - /** - * Records the list of audio packets in the sub-class specific audio format. - */ - protected abstract void record(List audioPackets) throws IOException; - - /** - * Starts this recorder as a scheduled thread running under the executor argument - */ - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - mTimeLastPacketReceived = System.currentTimeMillis(); - - if(mBufferProcessor == null) - { - mBufferProcessor = new BufferProcessor(); - } - - try - { - mFileOutputStream = new FileOutputStream(mPath.toFile()); - - /* Schedule the handler to run every half second */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); - } - catch(IOException io) - { - mLog.error("Error starting audio recorder [" + getPath().toString() + "]", io); - - mRunning.set(false); - } - } - } - - /** - * Processes the audio packet queue. - */ - private void processAudioPacketQueue() - { - mAudioPacketQueue.drainTo(mPacketsToProcess); - - if(!mPacketsToProcess.isEmpty()) - { - try - { - record(mPacketsToProcess); - - for(ReusableAudioPacket packet : mPacketsToProcess) - { - if(packet.getType() == ReusableAudioPacket.Type.AUDIO) - { - mSampleCount += packet.getAudioSamples().length; - } - - packet.decrementUserCount(); - } - } - catch(IOException ioe) - { - mLog.debug("Error while recording audio to [" + getPath().toString() + "] - stopping recorder"); - stop(); - } - - mPacketsToProcess.clear(); - } - } - - /** - * Disposes this audio recorder and prepares it for reclamation - */ - @Override - public void dispose() - { - stop(); - } - - /** - * Not implemented. Recorder modules are not appropriate for reset and reuse. - */ - @Override - public void reset() - { - } - - /** - * Flushes any remaining audio to the output file. This method should be implemented by subclasses where an audio - * converter may contain residual frame data that should be flushed to disk before closing the audio file. - */ - protected void flush() - { - } - - /** - * Drains the audio packet queue and records the audio packets to file - */ - public class BufferProcessor implements Runnable - { - private AtomicBoolean mProcessing = new AtomicBoolean(); - - public void run() - { - if(mProcessing.compareAndSet(false, true)) - { - processAudioPacketQueue(); - - //If we've been stopped or closed, finish the queue, close the recording, notify the listener, and - // cancel the future - if(!mRunning.get()) - { - //Allow sub-classes to flush remaining audio frame data to disk. - flush(); - - if(mFileOutputStream != null) - { - try - { - mFileOutputStream.flush(); - mFileOutputStream.close(); - } - catch(IOException e) - { - mLog.error("Error closing output stream", e); - } - } - - synchronized(mRecordingClosedListener) - { - if(mRecordingClosedListener != null) - { - mRecordingClosedListener.receive(AudioRecorder.this); - mRecordingClosedListener = null; - } - } - - if(mProcessorHandle != null) - { - mProcessorHandle.cancel(false); - mProcessorHandle = null; - } - } - - mProcessing.set(false); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java new file mode 100644 index 000000000..11018131d --- /dev/null +++ b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java @@ -0,0 +1,281 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.record; + +import io.github.dsheirer.audio.AudioSegment; +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.integer.IntegerIdentifier; +import io.github.dsheirer.identifier.string.StringIdentifier; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.util.StringUtils; +import io.github.dsheirer.util.ThreadPool; +import io.github.dsheirer.util.TimeStamp; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Monitors audio segments and upon completion records any audio segments that have been flagged as recordable + */ +public class AudioRecordingManager implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioRecordingManager.class); + private LinkedTransferQueue mCompletedAudioSegmentQueue = new LinkedTransferQueue<>(); + private ScheduledFuture mQueueProcessorHandle; + private UserPreferences mUserPreferences; + private int mUnknownAudioRecordingIndex = 1; + + /** + * Constructs an instance + * @param userPreferences to determine audio recording format + */ + public AudioRecordingManager(UserPreferences userPreferences) + { + mUserPreferences = userPreferences; + } + + /** + * Starts the manager and begins audio segment recording. + */ + public void start() + { + if(mQueueProcessorHandle == null) + { + mQueueProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new QueueProcessor(), + 0, 1, TimeUnit.SECONDS); + } + } + + /** + * Stops the manager and records any remaining queued audio segments. + */ + public void stop() + { + if(mQueueProcessorHandle != null) + { + mQueueProcessorHandle.cancel(true); + processAudioSegments(); + mQueueProcessorHandle = null; + } + } + + /** + * Primary receive method for incoming audio segments to be recorded + */ + @Override + public void receive(AudioSegment audioSegment) + { + audioSegment.completeProperty().addListener(new AudioSegmentCompletionMonitor(audioSegment)); + } + + /** + * Processes audio segments that have been flagged as complete. + * @param audioSegment + */ + public void processCompletedAudioSegment(AudioSegment audioSegment) + { + if(audioSegment.recordAudioProperty().get()) + { + mCompletedAudioSegmentQueue.add(audioSegment); + } + else + { + audioSegment.decrementConsumerCount(); + } + } + + /** + * Processes any queued audio segments + */ + private void processAudioSegments() + { + List audioSegments = new ArrayList<>(); + mCompletedAudioSegmentQueue.drainTo(audioSegments); + + if(!audioSegments.isEmpty()) + { + RecordFormat recordFormat = mUserPreferences.getRecordPreference().getAudioRecordFormat(); + + for(AudioSegment audioSegment: audioSegments) + { + Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat); + + try + { + AudioSegmentRecorder.record(audioSegment, path, recordFormat); + } + catch(IOException ioe) + { + mLog.error("Error recording audio segment to [" + path.toString() + "]"); + } + + audioSegment.decrementConsumerCount(); + } + } + } + + /** + * Base path to recordings folder + * @return + */ + public Path getRecordingBasePath() + { + return mUserPreferences.getDirectoryPreference().getDirectoryRecording(); + } + + /** + * Provides a formatted audio recording filename to use as the final audio filename. + */ + private Path getAudioRecordingPath(IdentifierCollection identifierCollection, RecordFormat recordFormat) + { + StringBuilder sb = new StringBuilder(); + + if(identifierCollection != null) + { + Identifier system = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY); + + if(system != null) + { + sb.append(((StringIdentifier)system).getValue()).append("_"); + } + + Identifier site = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SITE, Role.ANY); + + if(site != null) + { + sb.append(((StringIdentifier)site).getValue()).append("_"); + } + + Identifier channel = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL, Role.ANY); + + if(channel != null) + { + sb.append(((StringIdentifier)channel).getValue()).append("_"); + } + + Identifier to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO); + + if(to != null) + { + String toValue = ((IntegerIdentifier)to).getValue().toString().replace(":", ""); + sb.append("_TO_").append(toValue); + } + else + { + List toIdentifiers = identifierCollection.getIdentifiers(Role.TO); + + if(!toIdentifiers.isEmpty()) + { + sb.append("_TO_").append(toIdentifiers.get(0)).toString(); + } + } + + Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM); + + if(from != null) + { + String fromValue = ((IntegerIdentifier)from).getValue().toString().replace(":", ""); + sb.append("_FROM_").append(fromValue); + } + else + { + List fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM); + + if(!fromIdentifiers.isEmpty()) + { + sb.append("_FROM_").append(fromIdentifiers.get(0)).toString(); + } + } + } + else + { + sb.append("audio_recording_no_metadata_").append(mUnknownAudioRecordingIndex++); + + if(mUnknownAudioRecordingIndex < 0) + { + mUnknownAudioRecordingIndex = 1; + } + } + + StringBuilder sbFinal = new StringBuilder(); + sbFinal.append(TimeStamp.getTimeStamp("_")); + + //Remove any illegal filename characters + sbFinal.append(StringUtils.replaceIllegalCharacters(sb.toString())); + sbFinal.append(recordFormat.getExtension()); + + return getRecordingBasePath().resolve(sbFinal.toString()); + } + + + /** + * Audio segment completion monitor. Listens for the audio segment's complete flag to be set and then + * queues the audio segment for recording. + */ + public class AudioSegmentCompletionMonitor implements ChangeListener + { + private AudioSegment mAudioSegment; + + public AudioSegmentCompletionMonitor(AudioSegment audioSegment) + { + mAudioSegment = audioSegment; + } + + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) + { + mAudioSegment.completeProperty().removeListener(this); + processCompletedAudioSegment(mAudioSegment); + } + } + + /** + * Threaded queue processor to process/record each recordable audio segment + */ + public class QueueProcessor implements Runnable + { + @Override + public void run() + { + try + { + processAudioSegments(); + } + catch(Throwable t) + { + mLog.error("Error while processing queued audio segments to recordings", t); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/record/AudioSegmentRecorder.java b/src/main/java/io/github/dsheirer/record/AudioSegmentRecorder.java new file mode 100644 index 000000000..ca9fb1f20 --- /dev/null +++ b/src/main/java/io/github/dsheirer/record/AudioSegmentRecorder.java @@ -0,0 +1,134 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.record; + +import io.github.dsheirer.audio.AudioFormats; +import io.github.dsheirer.audio.AudioSegment; +import io.github.dsheirer.audio.convert.MP3AudioConverter; +import io.github.dsheirer.record.wave.AudioMetadata; +import io.github.dsheirer.record.wave.AudioMetadataUtils; +import io.github.dsheirer.record.wave.WaveWriter; +import io.github.dsheirer.sample.ConversionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Map; + +/** + * Recording utility for audio segments + */ +public class AudioSegmentRecorder +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioSegmentRecorder.class); + + public static final int MP3_BIT_RATE = 16; + public static final boolean CONSTANT_BIT_RATE = false; + + /** + * Records the audio segment to the specified path using the specified recording format + * @param audioSegment to record + * @param path for the recording + * @param recordFormat to use (WAVE, MP3) + * @throws IOException on any errors + */ + public static void record(AudioSegment audioSegment, Path path, RecordFormat recordFormat) throws IOException + { + switch(recordFormat) + { + case MP3: + recordMP3(audioSegment, path); + break; + case WAVE: + recordWAVE(audioSegment, path); + break; + default: + throw new IllegalArgumentException("Unrecognized recording format [" + recordFormat.name() + "]"); + } + } + + /** + * Records the audio segment as an MP3 file to the specified path. + * @param audioSegment to record + * @param path for the recording + * @throws IOException on any errors + */ + public static void recordMP3(AudioSegment audioSegment, Path path) throws IOException + { + if(audioSegment.hasAudio()) + { + OutputStream outputStream = new FileOutputStream(path.toFile()); + + //Write ID3 metadata + Map metadataMap = AudioMetadataUtils.getMetadataMap(audioSegment.getIdentifierCollection(), + audioSegment.getAliasList()); + + byte[] id3Bytes = AudioMetadataUtils.getMP3ID3(metadataMap); + outputStream.write(id3Bytes); + + //Convert audio to MP3 and write to file + MP3AudioConverter converter = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE); + byte[] mp3 = converter.convertAudio(audioSegment.getAudioBuffers()); + outputStream.write(mp3); + + byte[] lastFrame = converter.flush(); + + if(lastFrame != null && lastFrame.length > 0) + { + outputStream.write(lastFrame); + } + + outputStream.flush(); + outputStream.close(); + } + } + + /** + * Records the audio segment as a WAVe file to the specified path. + * @param audioSegment to record + * @param path for the recording + * @throws IOException on any errors + */ + public static void recordWAVE(AudioSegment audioSegment, Path path) throws IOException + { + if(audioSegment.hasAudio()) + { + WaveWriter writer = new WaveWriter(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, path); + + for(float[] audioBuffer: audioSegment.getAudioBuffers()) + { + writer.writeData(ConversionUtils.convertToSigned16BitSamples(audioBuffer)); + } + + Map metadataMap = AudioMetadataUtils.getMetadataMap(audioSegment.getIdentifierCollection(), + audioSegment.getAliasList()); + + ByteBuffer listChunk = AudioMetadataUtils.getLISTChunk(metadataMap); + byte[] id3Bytes = AudioMetadataUtils.getMP3ID3(metadataMap); + ByteBuffer id3Chunk = AudioMetadataUtils.getID3Chunk(id3Bytes); + writer.writeMetadata(listChunk, id3Chunk); + writer.close(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/record/RecordFormat.java b/src/main/java/io/github/dsheirer/record/RecordFormat.java new file mode 100644 index 000000000..a13c9ddd9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/record/RecordFormat.java @@ -0,0 +1,44 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.record; + +/** + * Audio recording formats + */ +public enum RecordFormat +{ + WAVE(".wav"), + MP3(".mp3"); + + private String mExtension; + + RecordFormat(String extension) + { + mExtension = extension; + } + + /** + * File extension + */ + public String getExtension() + { + return mExtension; + } +} diff --git a/src/main/java/io/github/dsheirer/record/RecorderFactory.java b/src/main/java/io/github/dsheirer/record/RecorderFactory.java index 32a4fe6b9..2689c9e03 100644 --- a/src/main/java/io/github/dsheirer/record/RecorderFactory.java +++ b/src/main/java/io/github/dsheirer/record/RecorderFactory.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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.record; @@ -28,14 +25,20 @@ import io.github.dsheirer.module.decode.p25.audio.P25P2CallSequenceRecorder; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.record.binary.BinaryRecorder; +import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; +import io.github.dsheirer.util.StringUtils; +import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; public class RecorderFactory { + public static final float BASEBAND_SAMPLE_RATE = 25000.0f; //Default sample rate - source can override + /** * Creates recorder modules based on the channel configuration details * @param recorderManager @@ -43,7 +46,7 @@ public class RecorderFactory * @param channel * @return */ - public static List getRecorders(RecorderManager recorderManager, UserPreferences userPreferences, Channel channel) + public static List getRecorders(UserPreferences userPreferences, Channel channel) { List recorderModules = new ArrayList<>(); @@ -54,26 +57,26 @@ public static List getRecorders(RecorderManager recorderManager, UserPre case BASEBAND: if(channel.isStandardChannel()) { - recorderModules.add(recorderManager.getBasebandRecorder(channel.toString())); + recorderModules.add(getBasebandRecorder(channel.toString(), userPreferences)); } break; case DEMODULATED_BIT_STREAM: if(channel.isStandardChannel() && channel.getDecodeConfiguration().getDecoderType().providesBitstream()) { - recorderModules.add(new BinaryRecorder(recorderManager.getRecordingBasePath(), + recorderModules.add(new BinaryRecorder(getRecordingBasePath(userPreferences), channel.toString(), channel.getDecodeConfiguration().getDecoderType().getProtocol())); } break; case TRAFFIC_BASEBAND: if(channel.isTrafficChannel()) { - recorderModules.add(recorderManager.getBasebandRecorder(channel.toString())); + recorderModules.add(getBasebandRecorder(channel.toString(), userPreferences)); } break; case TRAFFIC_DEMODULATED_BIT_STREAM: if(channel.isTrafficChannel() && channel.getDecodeConfiguration().getDecoderType().providesBitstream()) { - recorderModules.add(new BinaryRecorder(recorderManager.getRecordingBasePath(), + recorderModules.add(new BinaryRecorder(getRecordingBasePath(userPreferences), channel.toString(), channel.getDecodeConfiguration().getDecoderType().getProtocol())); } break; @@ -136,4 +139,24 @@ else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFre return recorderModules; } + + /** + * Base path to recordings folder + */ + public static Path getRecordingBasePath(UserPreferences userPreferences) + { + return userPreferences.getDirectoryPreference().getDirectoryRecording(); + } + + /** + * Constructs a baseband recorder for use in a processing chain. + */ + public static ComplexBufferWaveRecorder getBasebandRecorder(String channelName, UserPreferences userPreferences) + { + StringBuilder sb = new StringBuilder(); + sb.append(getRecordingBasePath(userPreferences)); + sb.append(File.separator).append(StringUtils.replaceIllegalCharacters(channelName)).append("_baseband"); + + return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString()); + } } diff --git a/src/main/java/io/github/dsheirer/record/RecorderManager.java b/src/main/java/io/github/dsheirer/record/RecorderManager.java deleted file mode 100644 index cf3df4ed2..000000000 --- a/src/main/java/io/github/dsheirer/record/RecorderManager.java +++ /dev/null @@ -1,371 +0,0 @@ -/* - * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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.record; - -import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.identifier.Form; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.IdentifierCollection; -import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.integer.IntegerIdentifier; -import io.github.dsheirer.identifier.string.StringIdentifier; -import io.github.dsheirer.preference.UserPreferences; -import io.github.dsheirer.record.wave.AudioPacketWaveRecorder; -import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; -import io.github.dsheirer.record.wave.WaveMetadata; -import io.github.dsheirer.sample.IOverflowListener; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.OverflowableTransferQueue; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; -import io.github.dsheirer.util.StringUtils; -import io.github.dsheirer.util.ThreadPool; -import io.github.dsheirer.util.TimeStamp; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public class RecorderManager implements Listener -{ - private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class); - - public static final float BASEBAND_SAMPLE_RATE = 25000.0f; //Default sample rate - source can override - public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds - - private Map mRecorders = new HashMap<>(); - private OverflowableTransferQueue mAudioPacketQueue = new OverflowableTransferQueue<>(1000, 100); - private List mAudioPackets = new ArrayList<>(); - private ScheduledFuture mBufferProcessorFuture; - private AliasModel mAliasModel; - private UserPreferences mUserPreferences; - private int mUnknownAudioRecordingIndex = 1; - - private boolean mCanStartNewRecorders = true; - - /** - * Audio recording manager. Monitors stream of audio packets produced by decoding channels and automatically starts - * audio recorders when the channel's metadata designates a call as recordable. Routes call audio to each recorder - * based on audio packet metadata. Recorders are shutdown when the channel sends an end-call audio packet - * indicating that the call is complete. A separate recording monitor periodically checks for idled recorders to - * be stopped for cases when the channel fails to send an end-call audio packet. - */ - public RecorderManager(AliasModel aliasModel, UserPreferences userPreferences) - { - mAliasModel = aliasModel; - mUserPreferences = userPreferences; - mAudioPacketQueue.setOverflowListener(new IOverflowListener() - { - @Override - public void sourceOverflow(boolean overflow) - { - if(overflow) - { - mLog.warn("overflow - audio packets will be dropped until recording catches up"); - } - else - { - mLog.info("audio recorder packet processing has returned to normal"); - } - } - }); - - mBufferProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(), 0, - 500, TimeUnit.MILLISECONDS); - } - - /** - * Prepares this class for shutdown - */ - public void dispose() - { - if(mBufferProcessorFuture != null) - { - mBufferProcessorFuture.cancel(true); - } - } - - /** - * Primary ingest point for audio packets from all decoding channels - * - * @param audioPacket to process - */ - @Override - public void receive(ReusableAudioPacket audioPacket) - { - if(audioPacket.hasIdentifierCollection() && audioPacket.isRecordable()) - { - mAudioPacketQueue.offer(audioPacket); - } - else - { - audioPacket.decrementUserCount(); - } - } - - /** - * Process any queued audio buffers and dispatch them to the audio recorders - */ - private void processBuffers() - { - mAudioPacketQueue.drainTo(mAudioPackets, 50); - - while(!mAudioPackets.isEmpty()) - { - for(ReusableAudioPacket audioPacket : mAudioPackets) - { - int audioChannelId = audioPacket.getAudioChannelId(); - - if(mRecorders.containsKey(audioChannelId)) - { - AudioPacketWaveRecorder recorder = mRecorders.get(audioChannelId); - - if(audioPacket.getType() == ReusableAudioPacket.Type.AUDIO) - { - audioPacket.incrementUserCount(); - recorder.receive(audioPacket); - } - else if(audioPacket.getType() == ReusableAudioPacket.Type.END) - { - AudioPacketWaveRecorder finished = mRecorders.remove(audioChannelId); - stopRecorder(finished); - } - } - else if(audioPacket.getType() == ReusableAudioPacket.Type.AUDIO) - { - if(mCanStartNewRecorders) - { - AudioPacketWaveRecorder recorder = null; - - try - { - recorder = new AudioPacketWaveRecorder(getTemporaryFilePath(audioPacket)); - - recorder.start(); - - audioPacket.incrementUserCount(); - recorder.receive(audioPacket); - mRecorders.put(audioPacket.getAudioChannelId(), recorder); - } - catch(Exception ioe) - { - mCanStartNewRecorders = false; - - mLog.error("Error attempting to start new audio wave recorder. All (future) audio recording " + - "is disabled", ioe); - - if(recorder != null) - { - stopRecorder(recorder); - } - } - } - } - - audioPacket.decrementUserCount(); - } - - mAudioPackets.clear(); - mAudioPacketQueue.drainTo(mAudioPackets, 50); - } - } - - private void stopRecorder(AudioPacketWaveRecorder recorder) - { - Path rename = getFinalFileName(recorder.getIdentifierCollection()); - - AliasList aliasList = null; - - if(recorder.getIdentifierCollection() != null) - { - aliasList = mAliasModel.getAliasList(recorder.getIdentifierCollection()); - } - - WaveMetadata waveMetadata = WaveMetadata.createFrom(recorder.getIdentifierCollection(), aliasList); - recorder.stop(rename, waveMetadata); - } - - /** - * Removes recorders that have not received any new audio buffers in the last 6 seconds. - */ - private void removeIdleRecorders() - { - Iterator> it = mRecorders.entrySet().iterator(); - - while(it.hasNext()) - { - Map.Entry entry = it.next(); - - if(entry.getValue().getLastBufferReceived() + IDLE_RECORDER_REMOVAL_THRESHOLD < System.currentTimeMillis()) - { - it.remove(); - stopRecorder(entry.getValue()); - } - } - } - - private Path getTemporaryFilePath(ReusableAudioPacket packet) - { - StringBuilder sb = new StringBuilder(); - sb.append(TimeStamp.getTimeStamp("-")); - sb.append("_audio_channel_"); - sb.append(packet.getAudioChannelId()); - sb.append(".tmp"); - return getRecordingBasePath().resolve(sb.toString()); - } - - /** - * Provides a formatted audio recording filename to use as the final audio filename. - * @return - */ - private Path getFinalFileName(IdentifierCollection identifierCollection) - { - StringBuilder sb = new StringBuilder(); - - if(identifierCollection != null) - { - Identifier system = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY); - - if(system != null) - { - sb.append(((StringIdentifier)system).getValue()).append("_"); - } - - Identifier site = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SITE, Role.ANY); - - if(site != null) - { - sb.append(((StringIdentifier)site).getValue()).append("_"); - } - - Identifier channel = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL, Role.ANY); - - if(channel != null) - { - sb.append(((StringIdentifier)channel).getValue()).append("_"); - } - - Identifier to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO); - - if(to != null) - { - String toValue = ((IntegerIdentifier)to).getValue().toString().replace(":", ""); - sb.append("_TO_").append(toValue); - } - else - { - List toIdentifiers = identifierCollection.getIdentifiers(Role.TO); - - if(!toIdentifiers.isEmpty()) - { - sb.append("_TO_").append(toIdentifiers.get(0)).toString(); - } - } - - Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM); - - if(from != null) - { - String fromValue = ((IntegerIdentifier)from).getValue().toString().replace(":", ""); - sb.append("_FROM_").append(fromValue); - } - else - { - List fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM); - - if(!fromIdentifiers.isEmpty()) - { - sb.append("_FROM_").append(fromIdentifiers.get(0)).toString(); - } - } - } - else - { - sb.append("audio_recording_no_metadata_").append(mUnknownAudioRecordingIndex++); - - if(mUnknownAudioRecordingIndex < 0) - { - mUnknownAudioRecordingIndex = 1; - } - } - - StringBuilder sbFinal = new StringBuilder(); - sbFinal.append(TimeStamp.getTimeStamp("_")); - - //Remove any illegal filename characters - sbFinal.append(StringUtils.replaceIllegalCharacters(sb.toString())); - sbFinal.append(".wav"); - - return getRecordingBasePath().resolve(sbFinal.toString()); - } - - /** - * Base path to recordings folder - * @return - */ - public Path getRecordingBasePath() - { - return mUserPreferences.getDirectoryPreference().getDirectoryRecording(); - } - - /** - * Constructs a baseband recorder for use in a processing chain. - */ - public ComplexBufferWaveRecorder getBasebandRecorder(String channelName) - { - StringBuilder sb = new StringBuilder(); - sb.append(getRecordingBasePath()); - sb.append(File.separator).append(StringUtils.replaceIllegalCharacters(channelName)).append("_baseband"); - - return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString()); - } - - /** - * Processes queued audio packets and distributes to each of the audio recorders. Removes any idle recorders - * that have not been updated according to an idle threshold period - */ - public class BufferProcessor implements Runnable - { - @Override - public void run() - { - try - { - processBuffers(); - removeIdleRecorders(); - } - catch(Throwable t) - { - mLog.error("Error while processing audio buffers", t); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/record/mp3/MP3Recorder.java b/src/main/java/io/github/dsheirer/record/mp3/MP3Recorder.java deleted file mode 100644 index fb5f4bb41..000000000 --- a/src/main/java/io/github/dsheirer/record/mp3/MP3Recorder.java +++ /dev/null @@ -1,99 +0,0 @@ -/******************************************************************************* - * sdrtrunk - * 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 - * - ******************************************************************************/ -package io.github.dsheirer.record.mp3; - -import io.github.dsheirer.audio.convert.MP3AudioConverter; -import io.github.dsheirer.record.AudioRecorder; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Path; -import java.util.List; - -/** - * MP3 recorder for converting 8 kHz PCM audio packets to MP3 and writing to .mp3 file. - */ -public class MP3Recorder extends AudioRecorder -{ - private final static Logger mLog = LoggerFactory.getLogger(MP3Recorder.class); - - public static final int MP3_BIT_RATE = 16; - public static final boolean CONSTANT_BIT_RATE = false; - - private MP3AudioConverter mMP3Converter; - - /** - * MP3 audio recorder module for converting audio packets to 16 kHz constant bit rate MP3 format and - * recording to a file. - * - * @param path to the output file. File name should include the .mp3 file extension. - */ - public MP3Recorder(Path path) - { - super(path); - - mMP3Converter = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE); - } - - @Override - protected void record(List audioPackets) throws IOException - { - OutputStream outputStream = getOutputStream(); - - if(outputStream != null) - { - processMetadata(audioPackets); - - byte[] mp3Audio = mMP3Converter.convert(audioPackets); - - outputStream.write(mp3Audio); - } - } - - @Override - protected void flush() - { - byte[] partialFrame = mMP3Converter.flush(); - - if(partialFrame != null && partialFrame.length > 0) - { - try - { - getOutputStream().write(partialFrame); - } - catch(IOException ioe) - { - mLog.error("Error writing final audio frame data to file", ioe); - } - } - } - - /** - * Processes audio metadata contained in the audio packets and converts the metadata to MP3 ID3 metadata tags and - * writes the ID3 tags to the output stream. - * @param audioPackets - */ - private void processMetadata(List audioPackets) - { - //TODO: detect metadata changes and write out ID3 tags to the MP3 stream - } -} diff --git a/src/main/java/io/github/dsheirer/record/wave/WaveMetadataType.java b/src/main/java/io/github/dsheirer/record/wave/AudioMetadata.java similarity index 58% rename from src/main/java/io/github/dsheirer/record/wave/WaveMetadataType.java rename to src/main/java/io/github/dsheirer/record/wave/AudioMetadata.java index 666e4455e..004bb28d5 100644 --- a/src/main/java/io/github/dsheirer/record/wave/WaveMetadataType.java +++ b/src/main/java/io/github/dsheirer/record/wave/AudioMetadata.java @@ -18,49 +18,25 @@ package io.github.dsheirer.record.wave; /** - * WAVE audio metadata tags. + * WAVE and ID3 audio metadata tags. * * Metadata tag details: * LIST: https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html#Info * ID3: http://id3.org/id3v2.3.0 * */ -public enum WaveMetadataType +public enum AudioMetadata { //Primary tags - ARTIST_NAME("TPE1", "IART", true), //System - ALBUM_TITLE("TALB", "IPRD", true), //Site - TRACK_TITLE("TIT2", "INAM", true), //Channel Name + ARTIST_NAME("TPE1", "IART", true), + ALBUM_TITLE("TALB", "IPRD", true), + GROUPING("TIT1", "ISBJ", true), + TRACK_TITLE("TIT2", "INAM", true), COMMENTS("COMM", "ICMT", true), DATE_CREATED("TDRC", "ICRD", true), GENRE("TCON", "IGNR", true), - SOFTWARE("TSSE", "ISFT", true), //sdrtrunk application name - SOURCE_FORM("TMED", "ISRF", true), //protocol - - //Secondary tags - ALIAS_LIST_NAME("TALN", "ALLN", false), - CHANNEL_FREQUENCY("TCHF", "CHFQ", false), - CHANNEL_ID("TCHI", "CHID", false), - CHANNEL_TIMESLOT("TCHT", "CHTS", false), - NETWORK_ID_1("TNT1", "NTW1", false), - NETWORK_ID_2("TNT2", "NTW2", false), - TALKGROUP_PRIMARY_PATCHED_1("TPP1", "TPP1", false), - TALKGROUP_PRIMARY_PATCHED_2("TPP2", "TPP2", false), - TALKGROUP_PRIMARY_PATCHED_3("TPP3", "TPP3", false), - TALKGROUP_PRIMARY_PATCHED_4("TPP4", "TPP4", false), - TALKGROUP_PRIMARY_PATCHED_5("TPP5", "TPP5", false), - TALKGROUP_PRIMARY_FROM("TPFM", "TPFM", false), - TALKGROUP_PRIMARY_FROM_ALIAS("TPFA", "TPFA", false), - TALKGROUP_PRIMARY_FROM_ICON("TPFI", "TPFI", false), - TALKGROUP_PRIMARY_TO("TPTO", "TPTO", false), - TALKGROUP_PRIMARY_TO_ALIAS("TOTA", "TPTA", false), - TALKGROUP_PRIMARY_TO_ICON("TPTI", "TPTI", false), - TALKGROUP_SECONDARY_FROM("TSFM", "TSFM", false), - TALKGROUP_SECONDARY_FROM_ALIAS("TSFA", "TSFA", false), - TALKGROUP_SECONDARY_FROM_ICON("TSFI", "TSFI", false), - TALKGROUP_SECONDARY_TO("TSTO", "TSTO", false), - TALKGROUP_SECONDARY_TO_ALIAS("TSTA", "TSTA", false), - TALKGROUP_SECONDARY_TO_ICON("TSTI", "TSTI", false); + YEAR("TYER", "ICOP", true), + COMPOSER("TCOM", "ISFT", true); private String mID3Tag; private String mLISTTag; @@ -72,7 +48,7 @@ public enum WaveMetadataType * @param listTag used in the LIST chunk * @param PrimaryTag indicates if this is a custom LIST tag (true) or standard LIST tag (false) */ - WaveMetadataType(String id3Tag, String listTag, boolean PrimaryTag) + AudioMetadata(String id3Tag, String listTag, boolean PrimaryTag) { mID3Tag = id3Tag; mLISTTag = listTag; diff --git a/src/main/java/io/github/dsheirer/record/wave/AudioMetadataUtils.java b/src/main/java/io/github/dsheirer/record/wave/AudioMetadataUtils.java new file mode 100644 index 000000000..3a8190e2f --- /dev/null +++ b/src/main/java/io/github/dsheirer/record/wave/AudioMetadataUtils.java @@ -0,0 +1,358 @@ +/* + * ****************************************************************************** + * sdrtrunk + * Copyright (C) 2014-2019 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.record.wave; + +import com.mpatric.mp3agic.ID3v24Tag; +import io.github.dsheirer.alias.Alias; +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.configuration.ChannelNameConfigurationIdentifier; +import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier; +import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier; +import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier; +import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier; +import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier; +import io.github.dsheirer.properties.SystemProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AudioMetadataUtils +{ + private final static Logger mLog = LoggerFactory.getLogger(AudioMetadataUtils.class); + + private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + private static final SimpleDateFormat YEAR_SDF = new SimpleDateFormat("yyyy"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final byte NULL_TERMINATOR = (byte)0x00; + private static final String LIST_CHUNK_IDENTIFIER = "LIST"; + private static final String ID3_CHUNK_IDENTIFIER = "id3 "; + private static final String INFO_TYPE_IDENTIFIER = "INFO"; + private static final String GENRE_SCANNER_AUDIO = "Scanner Audio"; + private static final String COMMENT_SEPARATOR = ";"; + + /** + * Audio Metadata Utilities. Supports creating a map of metadata key:value pairs and converting the metadata to + * WAV (RIFF LIST) and MP3 (ID3) metadata formats. + */ + private AudioMetadataUtils() + { + //No constructor - all static utils + } + + /** + * Creates a metadata map from the audio metadata argument + * @param identifierCollection to create the metadata from + * @return map of metadata tags to values + */ + public static Map getMetadataMap(IdentifierCollection identifierCollection, AliasList aliasList) + { + Map audioMetadata = new HashMap<>(); + StringBuilder comments = new StringBuilder(); + audioMetadata.put(AudioMetadata.COMPOSER, SystemProperties.getInstance().getApplicationName()); + String dateCreated = SDF.format(new Date(System.currentTimeMillis())); + audioMetadata.put(AudioMetadata.DATE_CREATED, dateCreated); + comments.append("Date:").append(dateCreated).append(COMMENT_SEPARATOR); + audioMetadata.put(AudioMetadata.YEAR, YEAR_SDF.format(new Date(System.currentTimeMillis()))); + audioMetadata.put(AudioMetadata.GENRE, GENRE_SCANNER_AUDIO); + + if(identifierCollection != null) + { + for(Identifier to: identifierCollection.getIdentifiers(Role.TO)) + { + StringBuilder sb = new StringBuilder(); + sb.append(to.toString()); + + List aliases = aliasList.getAliases(to); + + for(Alias alias: aliases) + { + sb.append(" ").append(alias.toString()); + } + + audioMetadata.put(AudioMetadata.TRACK_TITLE, sb.toString()); + break; + } + + for(Identifier from: identifierCollection.getIdentifiers(Role.FROM)) + { + StringBuilder sb = new StringBuilder(); + sb.append(from.toString()); + + List aliases = aliasList.getAliases(from); + + for(Alias alias: aliases) + { + sb.append(" ").append(alias.toString()); + } + + audioMetadata.put(AudioMetadata.ARTIST_NAME, sb.toString()); + break; + } + + Identifier system = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY); + if(system instanceof SystemConfigurationIdentifier) + { + String value = ((SystemConfigurationIdentifier)system).getValue(); + audioMetadata.put(AudioMetadata.GROUPING, value); + comments.append("System:").append(value).append(COMMENT_SEPARATOR); + } + + Identifier site = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SITE, Role.ANY); + if(site instanceof SiteConfigurationIdentifier) + { + comments.append("Site:").append(((SiteConfigurationIdentifier)site).getValue()).append(COMMENT_SEPARATOR); + } + + Identifier channel = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL, Role.ANY); + if(channel instanceof ChannelNameConfigurationIdentifier) + { + String value = ((ChannelNameConfigurationIdentifier)channel).getValue(); + audioMetadata.put(AudioMetadata.ALBUM_TITLE, value); + comments.append("Name:").append(value).append(COMMENT_SEPARATOR); + } + + Identifier decoder = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.DECODER_TYPE, + Role.ANY); + if(decoder instanceof DecoderTypeConfigurationIdentifier) + { + comments.append("Decoder:") + .append(((DecoderTypeConfigurationIdentifier)decoder).getValue().getDisplayString()) + .append(COMMENT_SEPARATOR); + } + + Identifier channelName = identifierCollection.getIdentifier(IdentifierClass.DECODER, Form.CHANNEL_NAME, Role.BROADCAST); + + if(channelName instanceof DecoderLogicalChannelNameIdentifier) + { + comments.append("Channel:").append(((DecoderLogicalChannelNameIdentifier)channelName).getValue()) + .append(COMMENT_SEPARATOR); + } + + Identifier frequency = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, + Form.CHANNEL_FREQUENCY, Role.ANY); + + if(frequency instanceof FrequencyConfigurationIdentifier) + { + comments.append("Frequency:").append(((FrequencyConfigurationIdentifier)frequency).getValue()).append(COMMENT_SEPARATOR); + } + + } + + audioMetadata.put(AudioMetadata.COMMENTS, comments.toString()); + + return audioMetadata; + } + + + /** + * Creates an ID3 V2.4 metadata chunk suitable for embedding in an .mp3 audio file + * @param metadataMap of tags and values + * @return byte buffer of metadata chunk + */ + public static byte[] getMP3ID3(Map metadataMap) + { + ID3v24Tag tag = new ID3v24Tag(); + + for(AudioMetadata metadata: metadataMap.keySet()) + { + switch(metadata) + { + case ALBUM_TITLE: + tag.setAlbum(metadataMap.get(metadata)); + break; + case ARTIST_NAME: + tag.setArtist(metadataMap.get(metadata)); + break; + case COMMENTS: + tag.setComment(metadataMap.get(metadata)); + break; + case COMPOSER: + tag.setComposer(metadataMap.get(metadata)); + break; + case DATE_CREATED: + tag.setDate(metadataMap.get(metadata)); + break; + case GENRE: + tag.setGenreDescription(metadataMap.get(metadata)); + break; + case GROUPING: + tag.setGrouping(metadataMap.get(metadata)); + break; + case TRACK_TITLE: + tag.setTitle(metadataMap.get(metadata)); + break; + case YEAR: + tag.setYear(metadataMap.get(metadata)); + break; + } + } + + try + { + return tag.toBytes(); + } + catch(Exception e) + { + mLog.error("Error creating MP3 ID3 tag bytes", e); + } + + return new byte[0]; + } + + /** + * Wraps the contents argument in a wave metadata id3 chunk tag + * @param contents of the ID3 metadata + * @return id3 header/length wrapped ID3 block + */ + public static ByteBuffer getID3Chunk(byte[] contents) + { + int length = contents.length; + + //Pad the block out to a multiple of 4 + if(length % 4 != 0) + { + length += (4 - (length % 4)); + } + + ByteBuffer chunk = ByteBuffer.allocate(length + 8).order(ByteOrder.LITTLE_ENDIAN); + chunk.put(ID3_CHUNK_IDENTIFIER.getBytes()); + chunk.putInt(length); + chunk.put(contents); + + return chunk; + } + + /** + * Creates a wave LIST chunk from the metadata tags + */ + public static ByteBuffer getLISTChunk(Map metadataMap) + { + ByteBuffer metadataBuffer = getLISTSubChunks(metadataMap); + int tagsLength = metadataBuffer.capacity(); + + int overallLength = tagsLength + 8; + + //Pad the overall length to make it an event 32-bit boundary + int padding = 0; + + if(overallLength % 4 != 0) + { + padding = 4 - (overallLength % 4); + } + + ByteBuffer chunk = ByteBuffer.allocate(overallLength + padding).order(ByteOrder.LITTLE_ENDIAN); + + chunk.put(LIST_CHUNK_IDENTIFIER.getBytes()); + chunk.putInt(tagsLength + padding); + chunk.put(metadataBuffer); + + chunk.position(0); + + return chunk; + } + + /** + * Formats all metadata key:value pairs in the metadata map into a wave INFO compatible format + */ + private static ByteBuffer getLISTSubChunks(Map metadataMap) + { + int length = INFO_TYPE_IDENTIFIER.length(); + + List buffers = new ArrayList<>(); + + //Add the primary tags first + for(Map.Entry entry: metadataMap.entrySet()) + { + if(entry.getKey().isPrimaryTag()) + { + ByteBuffer buffer = getLISTSubChunk(entry.getKey(), entry.getValue()); + length += buffer.capacity(); + buffers.add(buffer); + } + } + + for(Map.Entry entry: metadataMap.entrySet()) + { + if(!entry.getKey().isPrimaryTag()) + { + ByteBuffer buffer = getLISTSubChunk(entry.getKey(), entry.getValue()); + length += buffer.capacity(); + buffers.add(buffer); + } + } + + ByteBuffer joinedBuffer = ByteBuffer.allocate(length); + + joinedBuffer.put(INFO_TYPE_IDENTIFIER.getBytes()); + + for(ByteBuffer buffer: buffers) + { + joinedBuffer.put(buffer); + } + + joinedBuffer.position(0); + + return joinedBuffer; + } + + /** + * Converts the metadata type and value into a Wave LIST compatible byte array. + * + * @param type of metadata + * @param value for the metadata + * @return metadata and value formatted for wave chunk + */ + private static ByteBuffer getLISTSubChunk(AudioMetadata type, String value) + { + if(value != null && !value.isEmpty()) + { + //Length is 4 bytes for tag ID, 4 bytes for length, value, and null terminator + ByteBuffer encodedValue = UTF_8.encode(value); + int chunkLength = encodedValue.capacity() + 9; + + ByteBuffer buffer = ByteBuffer.allocate(chunkLength).order(ByteOrder.LITTLE_ENDIAN); + buffer.put(type.getLISTTag().getBytes()); + buffer.putInt(encodedValue.capacity() + 1); + buffer.put(encodedValue); + buffer.put(NULL_TERMINATOR); + + buffer.position(0); + + return buffer; + } + + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java b/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java deleted file mode 100644 index e41a20410..000000000 --- a/src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * ********************************************************************************************************************* - * 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 - * ********************************************************************************************************************* - */ -package io.github.dsheirer.record.wave; - -import io.github.dsheirer.audio.AudioFormats; -import io.github.dsheirer.identifier.IdentifierCollection; -import io.github.dsheirer.module.Module; -import io.github.dsheirer.sample.ConversionUtils; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue; -import io.github.dsheirer.sample.buffer.ReusableAudioPacket; -import io.github.dsheirer.util.ThreadPool; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.sound.sampled.AudioFormat; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * WAVE audio recorder module for recording audio buffers to a wave file - */ -public class AudioPacketWaveRecorder extends Module implements Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(AudioPacketWaveRecorder.class); - - private WaveWriter mWriter; - private Path mPath; - private AudioFormat mAudioFormat; - - private OverflowableReusableBufferTransferQueue mTransferQueue = - new OverflowableReusableBufferTransferQueue<>(500, 100); - private BufferProcessor mBufferProcessor; - private ScheduledFuture mProcessorHandle; - private long mLastBufferReceived; - private List mAudioPacketsToProcess = new ArrayList<>(); - private IdentifierCollection mIdentifierCollection; - private AtomicBoolean mRunning = new AtomicBoolean(); - - /** - * Wave audio recorder for AudioPackets - * - * @param path for the recording file - */ - public AudioPacketWaveRecorder(Path path) - { - mPath = path; - mAudioFormat = AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO; - } - - /** - * Identifier collection harvested from the most recent audio packet. - * @return identifier collection or null. - */ - public IdentifierCollection getIdentifierCollection() - { - return mIdentifierCollection; - } - - /** - * Indicates if the recorder is currently running. - */ - public boolean isRunning() - { - return mRunning.get(); - } - - /** - * Timestamp of when the latest buffer was received by this recorder - */ - public long getLastBufferReceived() - { - return mLastBufferReceived; - } - - public Path getPath() - { - return mPath; - } - - public void start() - { - if(mRunning.compareAndSet(false, true)) - { - if(mBufferProcessor == null) - { - mBufferProcessor = new BufferProcessor(); - } - - try - { - mWriter = new WaveWriter(mAudioFormat, mPath); - - /* Schedule the processor to run every 500 milliseconds */ - mProcessorHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); - } - catch(IOException io) - { - mLog.error("Error starting real buffer recorder", io); - } - } - } - - @Override - public void stop() - { - stop(null, null); - } - - /** - * Stops the recorder and optionally renames the file to the specified path argument and/or writes - * the metadata to the recording. - * - * Note: both renaming path and wave metadata can be null. If no renaming path is specified, the - * original path name will remain for the audio file. - * - * @param path (optional) to rename the audio file. - * @param waveMetadata (optional) to include in the recording - */ - public void stop(Path path, WaveMetadata waveMetadata) - { - if(mRunning.compareAndSet(true, false)) - { - if(mProcessorHandle != null) - { - mProcessorHandle.cancel(false); - mProcessorHandle = null; - } - - try - { - //Finish writing any residual audio buffers - write(); - - //Append the LIST and ID3 metadata to the end - if(waveMetadata != null) - { - mWriter.writeMetadata(waveMetadata); - } - - if(mWriter != null) - { - mWriter.close(path); - mWriter = null; - } - } - catch(IOException ioe) - { - mLog.error("Error writing final audio buffers to recording during shutdown", ioe); - } - } - } - - /** - * Primary input method for receiving audio packets to enqueue for later writing to the wav file. - */ - @Override - public void receive(ReusableAudioPacket audioPacket) - { - if(mRunning.get()) - { - mTransferQueue.offer(audioPacket); - mLastBufferReceived = System.currentTimeMillis(); - } - else - { - audioPacket.decrementUserCount(); - } - } - - @Override - public void dispose() - { - stop(); - } - - @Override - public void reset() - { - } - - /** - * Writes all audio currently in the queue to the file. Captures any audio metadata from the packet and retains a - * copy of the latest metadata to append to the end of the recording when stop() is invoked. - * - * @throws IOException if there are any errors writing the audio - */ - private synchronized void write() throws IOException - { - mTransferQueue.drainTo(mAudioPacketsToProcess); - - for(ReusableAudioPacket audioPacket: mAudioPacketsToProcess) - { - if(audioPacket.getType() == ReusableAudioPacket.Type.AUDIO) - { - if(audioPacket.hasIdentifierCollection()) - { - mIdentifierCollection = audioPacket.getIdentifierCollection(); - } - - mWriter.writeData(ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples())); - } - - try - { - audioPacket.decrementUserCount(); - } - catch(IllegalStateException ise) - { - mLog.error("Error while decrementing user count on audio packet while writing data to recording file", ise); - } - } - - mAudioPacketsToProcess.clear(); - } - - /** - * Scheduled runnable to periodically write the enqueued audio packets to the file - */ - public class BufferProcessor implements Runnable - { - public void run() - { - try - { - write(); - } - catch(IOException ioe) - { - /* Stop this module if/when we get an IO exception */ - mTransferQueue.clear(); - stop(); - - mLog.error("IO Exception while trying to write to the wave writer", ioe); - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/record/wave/WaveMetadata.java b/src/main/java/io/github/dsheirer/record/wave/WaveMetadata.java deleted file mode 100644 index 6beaadde6..000000000 --- a/src/main/java/io/github/dsheirer/record/wave/WaveMetadata.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 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.record.wave; - -import com.google.common.base.Joiner; -import io.github.dsheirer.alias.Alias; -import io.github.dsheirer.alias.AliasList; -import io.github.dsheirer.identifier.Form; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.IdentifierCollection; -import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.configuration.ChannelNameConfigurationIdentifier; -import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier; -import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier; -import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier; -import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier; -import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier; -import io.github.dsheirer.identifier.patch.PatchGroup; -import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; -import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; -import io.github.dsheirer.properties.SystemProperties; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.Charset; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class WaveMetadata -{ - private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmssSSS"); - private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); - private static final Charset UTF_8 = Charset.forName("UTF-8"); - private static final byte NULL_TERMINATOR = (byte)0x00; - private static final String LIST_CHUNK_IDENTIFIER = "LIST"; - private static final String INFO_TYPE_IDENTIFIER = "INFO"; - private static final String ID3_CHUNK_IDENTIFIER = "ID3 "; - private static final String ID3V2_IDENTIFIER = "ID3"; - private static final byte ID3V2_MAJOR_VERSION = (byte)0x4; - private static final byte ID3V2_MINOR_VERSION = (byte)0x0; - private static final byte ID3V2_FLAGS = (byte)0x0; - private static final byte ID3V2_ISO_8859_1_ENCODING = (byte)0x0; - - private Map mMetadataMap = new HashMap<>(); - - /** - * Audio Wave File - Metadata Chunk. Supports creating a map of metadata key:value pairs and - * converting the metadata to WAV audio recording INFO/LIST and ID3 v2.4.0 metadata chunks. - */ - public WaveMetadata() - { - } - - public ByteBuffer getID3Chunk() - { - ByteBuffer subChunks = getID3Frames(); - int subChunksLength = subChunks.capacity(); - - int overallLength = subChunksLength + 18; - - //Pad the overall length to make it an even 32-bit boundary - int padding = 0; - - if(overallLength % 4 != 0) - { - padding = 4 - (overallLength % 4); - } - - ByteBuffer chunk = ByteBuffer.allocate(overallLength + padding).order(ByteOrder.LITTLE_ENDIAN); - - int chunkLength = subChunksLength + 10 + padding; - - chunk.put(ID3_CHUNK_IDENTIFIER.getBytes()); - chunk.putInt(chunkLength); - chunk.put(ID3V2_IDENTIFIER.getBytes()); - chunk.put(ID3V2_MAJOR_VERSION); - chunk.put(ID3V2_MINOR_VERSION); - chunk.put(ID3V2_FLAGS); - chunk.put(getID3EncodedLength(subChunksLength)); - - chunk.put(subChunks); - - chunk.position(0); - - return chunk; - } - - /** - * Creates an ID3 v2.4.0 compatible frame set from the metadata - */ - public ByteBuffer getID3Frames() - { - int length = 0; - List buffers = new ArrayList<>(); - - //List the primary metadata tags first - for(Map.Entry entry: mMetadataMap.entrySet()) - { - if(entry.getKey().isPrimaryTag()) - { - ByteBuffer buffer = getID3Frame(entry.getKey(), entry.getValue()); - - if(buffer != null) - { - length += buffer.capacity(); - buffers.add(buffer); - } - } - } - - for(Map.Entry entry: mMetadataMap.entrySet()) - { - if(!entry.getKey().isPrimaryTag()) - { - ByteBuffer buffer = getID3Frame(entry.getKey(), entry.getValue()); - - if(buffer != null) - { - length += buffer.capacity(); - buffers.add(buffer); - } - } - } - - ByteBuffer concatenatedBuffer = ByteBuffer.allocate(length); - - for(ByteBuffer buffer: buffers) - { - concatenatedBuffer.put(buffer); - } - - concatenatedBuffer.position(0); - - return concatenatedBuffer; - } - - /** - * Creates an ID3 v2.4.0 compatible metadata frame - * @param type of metadata - * @param value of the metadata - * @return frame byte buffer or null if the value is empty or null - */ - public ByteBuffer getID3Frame(WaveMetadataType type, String value) - { - if(value != null && !value.isEmpty()) - { - ByteBuffer encodedValue = ISO_8859_1.encode(value); - - //Length is 4 bytes for tag ID, 4 bytes for length, value, and null terminator - int chunkLength = encodedValue.capacity() + 11; - - ByteBuffer buffer = ByteBuffer.allocate(chunkLength).order(ByteOrder.LITTLE_ENDIAN); - buffer.put(type.getID3Tag().getBytes()); - buffer.put(getID3EncodedLength(encodedValue.capacity() + 1)); - buffer.put(ID3V2_FLAGS); - buffer.put(ID3V2_FLAGS); - buffer.put(ID3V2_ISO_8859_1_ENCODING); - buffer.put(encodedValue); - - buffer.position(0); - - return buffer; - } - - return null; - } - - /** - * Creates a wave LIST chunk from the metadata tags - */ - public ByteBuffer getLISTChunk() - { - ByteBuffer metadataBuffer = getLISTSubChunks(); - int tagsLength = metadataBuffer.capacity(); - - int overallLength = tagsLength + 8; - - //Pad the overall length to make it an event 32-bit boundary - int padding = 0; - - if(overallLength % 4 != 0) - { - padding = 4 - (overallLength % 4); - } - - ByteBuffer chunk = ByteBuffer.allocate(overallLength + padding).order(ByteOrder.LITTLE_ENDIAN); - - chunk.put(LIST_CHUNK_IDENTIFIER.getBytes()); - chunk.putInt(tagsLength + padding); - chunk.put(metadataBuffer); - - chunk.position(0); - - return chunk; - } - - /** - * Formats all metadata key:value pairs in the metadata map into a wave INFO compatible format - */ - private ByteBuffer getLISTSubChunks() - { - int length = INFO_TYPE_IDENTIFIER.length(); - - List buffers = new ArrayList<>(); - - //Add the primary tags first - for(Map.Entry entry: mMetadataMap.entrySet()) - { - if(entry.getKey().isPrimaryTag()) - { - ByteBuffer buffer = getLISTSubChunk(entry.getKey(), entry.getValue()); - length += buffer.capacity(); - buffers.add(buffer); - } - } - - for(Map.Entry entry: mMetadataMap.entrySet()) - { - if(!entry.getKey().isPrimaryTag()) - { - ByteBuffer buffer = getLISTSubChunk(entry.getKey(), entry.getValue()); - length += buffer.capacity(); - buffers.add(buffer); - } - } - - ByteBuffer joinedBuffer = ByteBuffer.allocate(length); - - joinedBuffer.put(INFO_TYPE_IDENTIFIER.getBytes()); - - for(ByteBuffer buffer: buffers) - { - joinedBuffer.put(buffer); - } - - joinedBuffer.position(0); - - return joinedBuffer; - } - - /** - * Converts the metadata type and value into a Wave LIST compatible byte array. - * - * @param type of metadata - * @param value for the metadata - * @return metadata and value formatted for wave chunk - */ - private ByteBuffer getLISTSubChunk(WaveMetadataType type, String value) - { - if(value != null && !value.isEmpty()) - { - //Length is 4 bytes for tag ID, 4 bytes for length, value, and null terminator - ByteBuffer encodedValue = UTF_8.encode(value); - int chunkLength = encodedValue.capacity() + 9; - - ByteBuffer buffer = ByteBuffer.allocate(chunkLength).order(ByteOrder.LITTLE_ENDIAN); - buffer.put(type.getLISTTag().getBytes()); - buffer.putInt(encodedValue.capacity() + 1); - buffer.put(encodedValue); - buffer.put(NULL_TERMINATOR); - - buffer.position(0); - - return buffer; - } - - return null; - } - - /** - * Adds the metadata key:value pair to the metadata map. Note: since this is a map, any existing - * key:value will be overwritten. - * @param type identifying the metadata type - * @param value to associate with the metadata type - */ - public void add(WaveMetadataType type, String value) - { - if(value != null) - { - mMetadataMap.put(type, value); - } - } - - /** - * Creates a WAVE recording metadata chunk from the audio metadata argument - * @param identifierCollection to create the wave metadata from - * @return wave metadata instance - */ - public static WaveMetadata createFrom(IdentifierCollection identifierCollection, AliasList aliasList) - { - WaveMetadata waveMetadata = new WaveMetadata(); - - waveMetadata.add(WaveMetadataType.SOFTWARE, SystemProperties.getInstance().getApplicationName()); - waveMetadata.add(WaveMetadataType.DATE_CREATED, SDF.format(new Date(System.currentTimeMillis()))); - - if(identifierCollection != null) - { - Identifier system = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY); - if(system instanceof SystemConfigurationIdentifier) - { - waveMetadata.add(WaveMetadataType.ARTIST_NAME, ((SystemConfigurationIdentifier)system).getValue()); - } - - Identifier site = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.SITE, Role.ANY); - if(site instanceof SiteConfigurationIdentifier) - { - waveMetadata.add(WaveMetadataType.ALBUM_TITLE, ((SiteConfigurationIdentifier)site).getValue()); - } - - Identifier channel = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL_NAME, Role.ANY); - if(channel instanceof ChannelNameConfigurationIdentifier) - { - waveMetadata.add(WaveMetadataType.TRACK_TITLE, ((ChannelNameConfigurationIdentifier)channel).getValue()); - } - - Identifier decoder = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, Form.DECODER_TYPE, - Role.ANY); - if(decoder instanceof DecoderTypeConfigurationIdentifier) - { - waveMetadata.add(WaveMetadataType.COMMENTS, ((DecoderTypeConfigurationIdentifier)decoder).getValue().getDisplayString()); - } - - Identifier channelName = identifierCollection.getIdentifier(IdentifierClass.DECODER, Form.CHANNEL_NAME, Role.ANY); - - if(channelName instanceof DecoderLogicalChannelNameIdentifier) - { - waveMetadata.add(WaveMetadataType.CHANNEL_ID, ((DecoderLogicalChannelNameIdentifier)channelName).getValue()); - } - - Identifier frequency = identifierCollection.getIdentifier(IdentifierClass.CONFIGURATION, - Form.CHANNEL_FREQUENCY, Role.ANY); - - if(frequency instanceof FrequencyConfigurationIdentifier) - { - waveMetadata.add(WaveMetadataType.CHANNEL_FREQUENCY, - String.valueOf(((FrequencyConfigurationIdentifier)frequency).getValue())); - } - - for(Identifier identifier: identifierCollection.getIdentifiers(Form.TALKGROUP)) - { - if(identifier instanceof TalkgroupIdentifier && identifier.getRole() == Role.FROM) - { - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_FROM, - String.valueOf(((TalkgroupIdentifier)identifier).getValue())); - - List aliases = aliasList.getAliases(identifier); - - if(!aliases.isEmpty()) - { - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_FROM_ALIAS, Joiner.on(", ").skipNulls().join(aliases)); - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_FROM_ICON, aliases.get(0).getIconName()); - } - } - else if(identifier instanceof TalkgroupIdentifier && identifier.getRole() == Role.TO) - { - List aliases = aliasList.getAliases(identifier); - - if(!aliases.isEmpty()) - { - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO_ALIAS, Joiner.on(", ").skipNulls().join(aliases)); - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO_ICON, aliases.get(0).getIconName()); - } - } - else if(identifier instanceof PatchGroupIdentifier) - { - PatchGroup patchGroup = ((PatchGroupIdentifier)identifier).getValue(); - - StringBuilder sb = new StringBuilder(); - sb.append("P:").append(patchGroup.getPatchGroup()).append(patchGroup.getPatchedGroupIdentifiers()); - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO, sb.toString()); - - List aliases = aliasList.getAliases(identifier); - - if(!aliases.isEmpty()) - { - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO_ALIAS, Joiner.on(", ").skipNulls().join(aliases)); - waveMetadata.add(WaveMetadataType.TALKGROUP_PRIMARY_TO_ICON, aliases.get(0).getIconName()); - } - } - } - } - - return waveMetadata; - } - - /** - * Converts the integer length to an ID3 compatible length field where each byte only uses the 7 least significant - * bits for a maximum of 28 bits used out of the 32-bit representation. - * @param length - * @return - */ - public static byte[] getID3EncodedLength(int length) - { - byte[] value = new byte[4]; - value[0] = (byte)((length >> 21) & 0x7F); - value[1] = (byte)((length >> 14) & 0x7F); - value[2] = (byte)((length >> 7) & 0x7F); - value[3] = (byte)(length & 0x7F); - - return value; - } -} diff --git a/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java b/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java index ff4796fa0..edf0955ed 100644 --- a/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java +++ b/src/main/java/io/github/dsheirer/record/wave/WaveWriter.java @@ -279,13 +279,11 @@ private void openDataChunk() throws IOException } /** - * Writes the metadata to the end of the file if there is sufficient space without exceeding the - * max file size. + * Writes the wave LIST chunk and the ID3 metadata chunk to the end of the file if there is sufficient space + * without exceeding the max file size. */ - public void writeMetadata(WaveMetadata metadata) throws IOException + public void writeMetadata(ByteBuffer listChunk, ByteBuffer id3Chunk) throws IOException { - ByteBuffer listChunk = metadata.getLISTChunk(); - if(mFileChannel.size() + listChunk.capacity() >= mMaxSize) { throw new IOException("Cannot write LIST metadata chunk - insufficient file space remaining"); @@ -302,8 +300,6 @@ public void writeMetadata(WaveMetadata metadata) throws IOException updateTotalSize(); - ByteBuffer id3Chunk = metadata.getID3Chunk(); - if(mFileChannel.size() + id3Chunk.capacity() >= mMaxSize) { throw new IOException("Cannot write ID3 metadata chunk - insufficient file space remaining"); diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacket.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacket.java deleted file mode 100644 index f6a3b1289..000000000 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacket.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 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.sample.buffer; - -import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; -import io.github.dsheirer.alias.id.priority.Priority; -import io.github.dsheirer.identifier.IdentifierCollection; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Reusable audio packet that carries audio sample data, associated metadata and a type to indicate if this - * packet contains audio data or is an end-audio packet. - */ -public class ReusableAudioPacket extends AbstractReusableBuffer -{ - private Type mType = Type.AUDIO; - private float[] mAudioSamples; - - private IdentifierCollection mIdentifierCollection; - private int mChannelId = 0; - private int mMonitoringPriority = Priority.DEFAULT_PRIORITY; - private boolean mRecordable = false; - private List mBroadcastChannels = new ArrayList<>(); - - /** - * Constructs a reusable audio packet. This constructor is package private and should only be used by - * a reusable audio packet queue. - * - * @param bufferDisposedListener to be notified when all users have released the packet - * @param length of the sample buffer - */ - ReusableAudioPacket(IReusableBufferDisposedListener bufferDisposedListener, int length) - { - super(bufferDisposedListener); - resize(length); - } - - /** - * Resets the attributes for this packet to default audio priority, non-recordable and non-streamable. - */ - public void resetAttributes() - { - mChannelId = 0; - mMonitoringPriority = Priority.DEFAULT_PRIORITY; - mRecordable = false; - mBroadcastChannels.clear(); - } - - /** - * Identifier for the channel that produced this audio packet. This ID is used to identify all packets - * within a packet stream that come from the same channel. - * - * @return channel identifier - */ - public int getAudioChannelId() - { - return mChannelId; - } - - /** - * Sets the channel identifier for this audio packet - * - * @param channelId for this audio packet. - */ - public void setAudioChannelId(int channelId) - { - mChannelId = channelId; - } - - /** - * Monitoring (ie local playback) priority. - */ - public int getMonitoringPriority() - { - return mMonitoringPriority; - } - - /** - * Sets the monitoring priority - * - * @param priority for monitoring: - * - * -1 Do Not Monitor - * 0 Selected (monitor override) - * 1 Highest - * ... - * 100 Lowest - */ - public void setMonitoringPriority(int priority) - { - mMonitoringPriority = priority; - } - - /** - * Indicates if the monitoring priority of this audio packet is set to do not monitor. - */ - public boolean isDoNotMonitor() - { - return mMonitoringPriority == Priority.DO_NOT_MONITOR; - } - - /** - * Indicates if this audio packet should be recorded. - */ - public boolean isRecordable() - { - return mRecordable; - } - - /** - * Sets the recordable status for this audio packet. - */ - public void setRecordable(boolean recordable) - { - mRecordable = recordable; - } - - /** - * List of broadcast/streaming channels for this audio packet. - */ - public List getBroadcastChannels() - { - return mBroadcastChannels; - } - - /** - * Adds the broadcast channels to this metadata - */ - public void addBroadcastChannels(Collection channels) - { - for(BroadcastChannel channel: channels) - { - if(!mBroadcastChannels.contains(channel)) - { - mBroadcastChannels.add(channel); - } - } - } - - /** - * Indicates if this audio packet has one or more streaming/broadcast channels assigned. - */ - public boolean isStreamable() - { - return !mBroadcastChannels.isEmpty(); - } - - /** - * Indicates if this packet has associated identifier collection - */ - public boolean hasIdentifierCollection() - { - return mIdentifierCollection != null; - } - - /** - * Associated audio metadata and identifiers - */ - public IdentifierCollection getIdentifierCollection() - { - return mIdentifierCollection; - } - - /** - * Sets the metadata associated with this audio packet. - */ - public void setIdentifierCollection(IdentifierCollection identifierCollection) - { - mIdentifierCollection = identifierCollection; - } - - /** - * Indicates the type of audio packet - */ - public Type getType() - { - return mType; - } - - /** - * Sets the type of audio packet. - * - * This method is package private and is used by the reusable audio packet queue. - */ - void setType(Type type) - { - mType = type; - } - - /** - * PCM 8 kHz audio samples - */ - public float[] getAudioSamples() - { - return mAudioSamples; - } - - /** - * Indicates if this audio packet contains audio samples. - */ - public boolean hasAudioSamples() - { - return mType != null && mType == Type.AUDIO && mAudioSamples != null; - } - - /** - * Resizes the internal audio sample buffer. - */ - public void resize(int length) - { - if(mAudioSamples == null || mAudioSamples.length != length) - { - mAudioSamples = new float[length]; - } - } - - /** - * Loads a copy of the float sample data from the reusable buffer into this audio packet, resizing the - * internal audio sample buffer as necessary. - * - * @param reusableFloatBuffer to load audio sample data from - */ - public void loadAudioFrom(ReusableFloatBuffer reusableFloatBuffer) - { - if(reusableFloatBuffer.getSamples().length != mAudioSamples.length) - { - resize(reusableFloatBuffer.getSamples().length); - } - - System.arraycopy(reusableFloatBuffer.getSamples(), 0, mAudioSamples, 0, reusableFloatBuffer.getSamples().length); - } - - /** - * Loads the audio sample array into this buffer. - * - * Note: this method is used for compatibility with legacy audio converters that have not been updated to - * use reusable audio packets and is therefore deprecated and will be removed once converters (ie JMBE) have - * been updated. - * - * @param audio to load - */ - public void loadAudioFrom(float[] audio) - { - mAudioSamples = audio; - } - - /** - * Audio Packet Type - */ - public enum Type - { - AUDIO, - END; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacketQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacketQueue.java deleted file mode 100644 index cb196fbae..000000000 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacketQueue.java +++ /dev/null @@ -1,77 +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 - * - ******************************************************************************/ -package io.github.dsheirer.sample.buffer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Queue for creating and reusing/managing reusable audio packets. - */ -public class ReusableAudioPacketQueue extends AbstractReusableBufferQueue -{ - private final static Logger mLog = LoggerFactory.getLogger(ReusableAudioPacketQueue.class); - - public ReusableAudioPacketQueue(String debugName) - { - super(debugName); - } - - /** - * Provides a reusable buffer from an internal recycling queue, or creates a new buffer if there are currently no - * buffers available for reuse. - * - * @param size of the samples that will be loaded into the buffer - * @return a reusable buffer - */ - public ReusableAudioPacket getBuffer(int size) - { - ReusableAudioPacket buffer = getRecycledBuffer(); - - if(buffer == null) - { - buffer = new ReusableAudioPacket(this, size); - buffer.setDebugName("Owner:" + getDebugName()); - incrementBufferCount(); - } - - buffer.setType(ReusableAudioPacket.Type.AUDIO); - buffer.resize(size); - buffer.incrementUserCount(); - - return buffer; - } - - /** - * Provides an end-audio packet that will not contain any audio samples. - */ - public ReusableAudioPacket getEndAudioBuffer() - { - ReusableAudioPacket buffer = getRecycledBuffer(); - - if(buffer == null) - { - buffer = new ReusableAudioPacket(this, 0); - buffer.setDebugName("Owner:" + getDebugName()); - incrementBufferCount(); - } - - buffer.setType(ReusableAudioPacket.Type.END); - buffer.incrementUserCount(); - - return buffer; - } -} diff --git a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java index 229b9387c..7c4373f1e 100644 --- a/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java +++ b/src/main/java/io/github/dsheirer/sample/buffer/ReusableBufferQueue.java @@ -1,18 +1,21 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 Dennis Sheirer +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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 - * - ******************************************************************************/ + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ package io.github.dsheirer.sample.buffer; import org.slf4j.Logger; @@ -51,4 +54,27 @@ public ReusableFloatBuffer getBuffer(int size) return buffer; } + + /** + * Creates or reuses a buffer and loads it with the samples and timestamp and increments user count to one. + * @param samples to load + * @param timestamp to set + * @return loaded buffer with user count set to one + */ + public ReusableFloatBuffer getBuffer(float[] samples, long timestamp) + { + ReusableFloatBuffer buffer = getRecycledBuffer(); + + if(buffer == null) + { + buffer = new ReusableFloatBuffer(this, new float[samples.length]); + buffer.setDebugName("Owner:" + getDebugName()); + incrementBufferCount(); + } + + buffer.reloadFrom(samples, timestamp); + buffer.incrementUserCount(); + + return buffer; + } } diff --git a/src/main/java/io/github/dsheirer/source/SourceManager.java b/src/main/java/io/github/dsheirer/source/SourceManager.java index 2ea29479f..0ff9d40ed 100644 --- a/src/main/java/io/github/dsheirer/source/SourceManager.java +++ b/src/main/java/io/github/dsheirer/source/SourceManager.java @@ -35,7 +35,6 @@ public class SourceManager { - private MixerManager mMixerManager; private RecordingSourceManager mRecordingSourceManager; private TunerManager mTunerManager; private TunerModel mTunerModel; @@ -43,9 +42,8 @@ public class SourceManager public SourceManager(TunerModel tunerModel, SettingsManager settingsManager, UserPreferences userPreferences) { mTunerModel = tunerModel; - mMixerManager = new MixerManager(); mRecordingSourceManager = new RecordingSourceManager(settingsManager); - mTunerManager = new TunerManager(mMixerManager, tunerModel, userPreferences); + mTunerManager = new TunerManager(tunerModel, userPreferences); //TODO: change mixer & recording managers to be models and hand them //in via the constructor. Perform loading outside of this class. @@ -60,11 +58,6 @@ public void shutdown() mTunerManager.dispose(); } - public MixerManager getMixerManager() - { - return mMixerManager; - } - public RecordingSourceManager getRecordingSourceManager() { return mRecordingSourceManager; @@ -87,7 +80,7 @@ public Source getSource(SourceConfiguration config, ChannelSpecification channel switch(config.getSourceType()) { case MIXER: - retVal = mMixerManager.getSource(config); + retVal = MixerManager.getSource(config); break; case TUNER: if(config instanceof SourceConfigTuner) diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerChannelConfiguration.java b/src/main/java/io/github/dsheirer/source/mixer/MixerChannelConfiguration.java index c54c5b81d..67a8b211c 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/MixerChannelConfiguration.java +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerChannelConfiguration.java @@ -1,76 +1,108 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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.mixer; import javax.sound.sampled.Mixer; public class MixerChannelConfiguration { - private Mixer mMixer; - private MixerChannel mMixerChannel; - - public MixerChannelConfiguration( Mixer mixer, MixerChannel channel ) - { - mMixer = mixer; - mMixerChannel = channel; - } - - public Mixer getMixer() - { - return mMixer; - } - - public MixerChannel getMixerChannel() - { - return mMixerChannel; - } - - public boolean matches( String mixer, String channels ) - { - return mixer != null && - channels != null && - mMixer.getMixerInfo().getName().contentEquals( mixer ) && - mMixerChannel.name().contentEquals( channels ); - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - - sb.append( mMixer.getMixerInfo().getName() ); - sb.append( " - " ); - sb.append( mMixerChannel.name() ); - - return sb.toString(); - } + private Mixer mMixer; + private MixerChannel mMixerChannel; + + public MixerChannelConfiguration(Mixer mixer, MixerChannel channel) + { + mMixer = mixer; + mMixerChannel = channel; + } + + public Mixer getMixer() + { + return mMixer; + } + + public MixerChannel getMixerChannel() + { + return mMixerChannel; + } + + public boolean matches(String mixer, String channels) + { + return mixer != null && + channels != null && + mMixer.getMixerInfo().getName().contentEquals(mixer) && + mMixerChannel.name().contentEquals(channels); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append(mMixer.getMixerInfo().getName()); + sb.append(" - "); + sb.append(mMixerChannel.name()); - @Override - public int hashCode() - { - final int prime = 31; - int result = 1; - result = prime * result + ( ( mMixer == null ) ? 0 : mMixer.hashCode() ); - result = prime * result - + ( ( mMixerChannel == null ) ? 0 : mMixerChannel.hashCode() ); - return result; - } + return sb.toString(); + } - @Override - public boolean equals( Object obj ) - { - if ( this == obj ) + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((mMixer == null) ? 0 : mMixer.hashCode()); + result = prime * result + + ((mMixerChannel == null) ? 0 : mMixerChannel.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if(this == obj) + { return true; - if ( obj == null ) - return false; - if ( getClass() != obj.getClass() ) + } + if(obj == null) + { return false; - MixerChannelConfiguration other = (MixerChannelConfiguration) obj; - if ( mMixer == null ) + } + if(getClass() != obj.getClass()) { - if ( other.mMixer != null ) + return false; + } + MixerChannelConfiguration other = (MixerChannelConfiguration)obj; + if(mMixer == null) + { + if(other.mMixer != null) + { return false; - } else if ( !mMixer.equals( other.mMixer ) ) + } + } + else if(!mMixer.equals(other.mMixer)) + { return false; - if ( mMixerChannel != other.mMixerChannel ) + } + if(mMixerChannel != other.mMixerChannel) + { return false; - return true; - } + } + return true; + } } diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java b/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java index 508269db0..6673921ec 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerManager.java @@ -36,26 +36,19 @@ import javax.sound.sampled.TargetDataLine; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; import java.util.List; -import java.util.Map; +/** + * Utility class for accessing input, output and tuner mixers + */ public class MixerManager { private final static Logger mLog = LoggerFactory.getLogger(MixerManager.class); - private List mInputMixers = new ArrayList<>(); - private List mOutputMixers = new ArrayList<>(); - private Map mMixerTuners = new HashMap<>(); + public MixerManager() {} - public MixerManager() - { - loadMixers(); - } - - public RealMixerSource getSource(SourceConfiguration config) + public static RealMixerSource getSource(SourceConfiguration config) { RealMixerSource retVal = null; @@ -131,14 +124,31 @@ public RealMixerSource getSource(SourceConfiguration config) return null; } - public InputMixerConfiguration[] getInputMixers() + public static List getInputMixers() { - return mInputMixers.toArray(new InputMixerConfiguration[mInputMixers.size()]); + List inputMixers = new ArrayList<>(); + + for(Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) + { + Mixer mixer = AudioSystem.getMixer(mixerInfo); + + if(mixer != null) + { + EnumSet inputChannels = getSupportedTargetChannels(mixer); + + if(inputChannels != null) + { + inputMixers.add(new InputMixerConfiguration(mixer, inputChannels)); + } + } + } + + return inputMixers; } - public InputMixerConfiguration getInputMixer(String name) + public static InputMixerConfiguration getInputMixer(String name) { - for(InputMixerConfiguration mixer : mInputMixers) + for(InputMixerConfiguration mixer : getInputMixers()) { if(mixer.getMixerName().contentEquals(name)) { @@ -149,67 +159,50 @@ public InputMixerConfiguration getInputMixer(String name) return null; } - public MixerChannelConfiguration[] getOutputMixers() + public static MixerChannelConfiguration getDefaultOutputMixer() { - return mOutputMixers.toArray(new MixerChannelConfiguration[mOutputMixers.size()]); - } + List outputMixers = getOutputMixers(); - public Collection getMixerTunerDataLines() - { - if(mMixerTuners.isEmpty()) + if(outputMixers.size() >= 1) { - return Collections.emptyList(); - } - else - { - return mMixerTuners.values(); + return outputMixers.get(0); } + + return null; } - private void loadMixers() + public static List getOutputMixers() { - StringBuilder sb = new StringBuilder(); - - sb.append("loading system mixer devices\n"); + List outputMixers = new ArrayList<>(); for(Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) { - //Sort between the mixers and the tuner mixers, and load each - MixerTunerType mixerTunerType = MixerTunerType.getMixerTunerType(mixerInfo); + Mixer mixer = AudioSystem.getMixer(mixerInfo); - if(mixerTunerType == MixerTunerType.UNKNOWN) + if(mixer != null) { - Mixer mixer = AudioSystem.getMixer(mixerInfo); + List outputChannels = getSupportedSourceChannels(mixer); - if(mixer != null) + for(MixerChannel channel : outputChannels) { - EnumSet inputChannels = getSupportedTargetChannels(mixer); - - if(inputChannels != null) - { - mInputMixers.add(new InputMixerConfiguration(mixer, inputChannels)); + outputMixers.add(new MixerChannelConfiguration(mixer, channel)); + } + } + } - sb.append("\t[LOADED] Input: " + mixerInfo.getName() + " CHANNELS: " + inputChannels + "\n"); - } + return outputMixers; + } - EnumSet outputChannels = getSupportedSourceChannels(mixer); + public static Collection getMixerTunerDataLines() + { + List tuners = new ArrayList<>(); - if(outputChannels != null) - { - for(MixerChannel channel : outputChannels) - { - mOutputMixers.add(new MixerChannelConfiguration(mixer, channel)); - } + for(Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) + { + //Sort between the mixers and the tuner mixers, and load each + MixerTunerType mixerTunerType = MixerTunerType.getMixerTunerType(mixerInfo); - sb.append("\t[LOADED] Output: " + mixerInfo.getName() + " CHANNELS: " + outputChannels + "\n"); - } - } - else - { - sb.append("\t[NOT LOADED] Mixer:" + mixerInfo.getName() + " - couldn't get mixer\n"); - } - } - else + if(mixerTunerType != MixerTunerType.UNKNOWN) { TargetDataLine tdl = getTargetDataLine(mixerInfo, mixerTunerType.getAudioFormat()); @@ -219,28 +212,17 @@ private void loadMixers() { case FUNCUBE_DONGLE_PRO: case FUNCUBE_DONGLE_PRO_PLUS: - mMixerTuners.put(mixerInfo.getName(), - new MixerTunerDataLine(tdl, mixerTunerType)); - sb.append("\t[LOADED] FunCube Dongle Mixer Tuner:" + mixerInfo.getName() + "[" + mixerTunerType.getDisplayString() + "]\n"); - break; - case UNKNOWN: - default: - sb.append("\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - unrecognized tuner type\n"); + tuners.add(new MixerTunerDataLine(tdl, mixerTunerType)); break; } } } } - mLog.info(sb.toString()); - } - - public Map getMixerTuners() - { - return mMixerTuners; + return tuners; } - private TargetDataLine getTargetDataLine(Mixer.Info mixerInfo, AudioFormat format) + private static TargetDataLine getTargetDataLine(Mixer.Info mixerInfo, AudioFormat format) { TargetDataLine retVal = null; @@ -263,7 +245,7 @@ private TargetDataLine getTargetDataLine(Mixer.Info mixerInfo, AudioFormat forma return retVal; } - private EnumSet getSupportedTargetChannels(Mixer mixer) + private static EnumSet getSupportedTargetChannels(Mixer mixer) { DataLine.Info stereoInfo = new DataLine.Info(TargetDataLine.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO); @@ -296,8 +278,10 @@ else if(monoSupported) * (MONO and/or STEREO) supported by the mixer, or null if the mixer doesn't * have any source data lines. */ - private EnumSet getSupportedSourceChannels(Mixer mixer) + private static List getSupportedSourceChannels(Mixer mixer) { + List channels = new ArrayList<>(); + DataLine.Info stereoInfo = new DataLine.Info(SourceDataLine.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO); @@ -307,20 +291,17 @@ private EnumSet getSupportedSourceChannels(Mixer mixer) boolean monoSupported = mixer.isLineSupported(monoInfo); - if(stereoSupported && monoSupported) + if(stereoSupported) { - return EnumSet.of(MixerChannel.MONO, MixerChannel.STEREO); + channels.add(MixerChannel.STEREO); } - else if(stereoSupported) - { - return EnumSet.of(MixerChannel.STEREO); - } - else if(monoSupported) + + if(monoSupported) { - return EnumSet.of(MixerChannel.MONO); + channels.add(MixerChannel.MONO); } - return null; + return channels; } public static String getMixerDevices() @@ -369,7 +350,7 @@ public static String getMixerDevices() return sb.toString(); } - public class InputMixerConfiguration + public static class InputMixerConfiguration { private Mixer mMixer; private EnumSet mChannels; @@ -405,4 +386,12 @@ public String toString() return mMixer.getMixerInfo().getName(); } } + + public static void main(String[] args) + { + for(MixerChannelConfiguration config : getOutputMixers()) + { + mLog.debug(config.toString()); + } + } } diff --git a/src/main/java/io/github/dsheirer/source/mixer/MixerSourceEditor.java b/src/main/java/io/github/dsheirer/source/mixer/MixerSourceEditor.java index b96303827..213beca20 100644 --- a/src/main/java/io/github/dsheirer/source/mixer/MixerSourceEditor.java +++ b/src/main/java/io/github/dsheirer/source/mixer/MixerSourceEditor.java @@ -1,17 +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 ******************************************************************************/ @@ -24,7 +24,9 @@ import io.github.dsheirer.source.config.SourceConfiguration; import net.miginfocom.swing.MigLayout; -import javax.swing.*; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JLabel; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.EnumSet; @@ -33,140 +35,141 @@ public class MixerSourceEditor extends Editor { private static final long serialVersionUID = 1L; private JComboBox mComboMixers; - private JComboBoxmComboChannels; - + private JComboBox mComboChannels; + private SourceManager mSourceManager; - public MixerSourceEditor( SourceManager sourceManager ) - { - mSourceManager = sourceManager; - init(); - } - - private void init() - { - setLayout( new MigLayout( "insets 0 0 0 0,wrap 4", "[right][grow,fill][right][grow,fill]", "" ) ); - - mComboMixers = new JComboBox(); - MixerManager.InputMixerConfiguration[] inputMixers = mSourceManager.getMixerManager().getInputMixers(); - mComboMixers.setModel( new DefaultComboBoxModel( inputMixers ) ); - mComboMixers.setEnabled( false ); - mComboMixers.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - MixerManager.InputMixerConfiguration selected = (MixerManager.InputMixerConfiguration)mComboMixers.getSelectedItem(); - - EnumSet channels = selected.getChannels(); - - mComboChannels.setModel( new DefaultComboBoxModel( - channels.toArray( new MixerChannel[ channels.size() ] ) ) ); - - repaint(); - - setModified( true ); - } - } ); - - add( new JLabel( "Mixer:" ) ); - add( mComboMixers ); - - mComboChannels = new JComboBox(); - - MixerManager.InputMixerConfiguration selected = (MixerManager.InputMixerConfiguration)mComboMixers.getSelectedItem(); - - if( selected != null ) - { - EnumSet channels = selected.getChannels(); - - mComboChannels.setModel( new DefaultComboBoxModel( - channels.toArray( new MixerChannel[ channels.size() ] ) ) ); - } - - mComboChannels.setEnabled( false ); - mComboChannels.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent e ) - { - setModified( true ); - } - } ); - - add( new JLabel( "Channel:" ) ); - add( mComboChannels ); - } - - @Override - public void setItem( Channel item ) - { - super.setItem( item ); - - if( hasItem() ) - { - mComboMixers.setEnabled( true ); - mComboChannels.setEnabled( true ); - - SourceConfiguration config = item.getSourceConfiguration(); - - if( config instanceof SourceConfigMixer) - { - SourceConfigMixer mixer = (SourceConfigMixer)config; - - for( MixerManager.InputMixerConfiguration inputMixer: mSourceManager.getMixerManager().getInputMixers() ) - { - if( inputMixer.getMixerName().equalsIgnoreCase( mixer.getMixer() ) ) - { - mComboMixers.setSelectedItem( inputMixer ); - - for( MixerChannel channel: inputMixer.getChannels() ) - { - if( channel.getLabel() != null && mixer.getChannel() != null ) - { - if( channel.getLabel().equalsIgnoreCase( mixer.getChannel().getLabel() ) ) - { - mComboChannels.setSelectedItem( channel ); - } - } - } - - continue; - } - } - - setModified( false ); - } - else - { - setModified( true ); - } - } - else - { - mComboMixers.setEnabled( false ); - mComboChannels.setEnabled( false ); - - setModified( false ); - } - } - - public void save() - { - if( hasItem() && isModified() ) - { - SourceConfigMixer config = new SourceConfigMixer(); - - MixerManager.InputMixerConfiguration selected = - (MixerManager.InputMixerConfiguration)mComboMixers.getSelectedItem(); - config.setMixer( selected.getMixerName() ); - - MixerChannel channel = (MixerChannel)mComboChannels.getSelectedItem(); - config.setChannel( channel ); - - getItem().setSourceConfiguration( config ); - } - - setModified( false ); - } + public MixerSourceEditor(SourceManager sourceManager) + { + mSourceManager = sourceManager; + init(); + } + + private void init() + { + setLayout(new MigLayout("insets 0 0 0 0,wrap 4", "[right][grow,fill][right][grow,fill]", "")); + + mComboMixers = new JComboBox(); + DefaultComboBoxModel model = new DefaultComboBoxModel<>(); + model.addAll(MixerManager.getInputMixers()); + mComboMixers.setModel(model); + mComboMixers.setEnabled(false); + mComboMixers.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + MixerManager.InputMixerConfiguration selected = (MixerManager.InputMixerConfiguration)mComboMixers.getSelectedItem(); + + EnumSet channels = selected.getChannels(); + + mComboChannels.setModel(new DefaultComboBoxModel( + channels.toArray(new MixerChannel[channels.size()]))); + + repaint(); + + setModified(true); + } + }); + + add(new JLabel("Mixer:")); + add(mComboMixers); + + mComboChannels = new JComboBox(); + + MixerManager.InputMixerConfiguration selected = (MixerManager.InputMixerConfiguration)mComboMixers.getSelectedItem(); + + if(selected != null) + { + EnumSet channels = selected.getChannels(); + + mComboChannels.setModel(new DefaultComboBoxModel( + channels.toArray(new MixerChannel[channels.size()]))); + } + + mComboChannels.setEnabled(false); + mComboChannels.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + setModified(true); + } + }); + + add(new JLabel("Channel:")); + add(mComboChannels); + } + + @Override + public void setItem(Channel item) + { + super.setItem(item); + + if(hasItem()) + { + mComboMixers.setEnabled(true); + mComboChannels.setEnabled(true); + + SourceConfiguration config = item.getSourceConfiguration(); + + if(config instanceof SourceConfigMixer) + { + SourceConfigMixer mixer = (SourceConfigMixer)config; + + for(MixerManager.InputMixerConfiguration inputMixer : MixerManager.getInputMixers()) + { + if(inputMixer.getMixerName().equalsIgnoreCase(mixer.getMixer())) + { + mComboMixers.setSelectedItem(inputMixer); + + for(MixerChannel channel : inputMixer.getChannels()) + { + if(channel.getLabel() != null && mixer.getChannel() != null) + { + if(channel.getLabel().equalsIgnoreCase(mixer.getChannel().getLabel())) + { + mComboChannels.setSelectedItem(channel); + } + } + } + + continue; + } + } + + setModified(false); + } + else + { + setModified(true); + } + } + else + { + mComboMixers.setEnabled(false); + mComboChannels.setEnabled(false); + + setModified(false); + } + } + + public void save() + { + if(hasItem() && isModified()) + { + SourceConfigMixer config = new SourceConfigMixer(); + + MixerManager.InputMixerConfiguration selected = + (MixerManager.InputMixerConfiguration)mComboMixers.getSelectedItem(); + config.setMixer(selected.getMixerName()); + + MixerChannel channel = (MixerChannel)mComboChannels.getSelectedItem(); + config.setChannel(channel); + + getItem().setSourceConfiguration(config); + } + + setModified(false); + } } 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 d07d5939f..c471d33e5 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -1,27 +1,25 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * ***************************************************************************** + * 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; -import io.github.dsheirer.record.RecorderManager; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.record.RecorderFactory; import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.IReusableComplexBufferProvider; @@ -476,11 +474,11 @@ public boolean isTunedFor(SortedSet tunerChannels) * Records the complex I/Q buffers produced by the tuner * @param recorderManager to obtain a baseband recorder */ - public void startRecorder(RecorderManager recorderManager) + public void startRecorder(UserPreferences userPreferences) { if(!isRecording()) { - mRecorder = recorderManager.getBasebandRecorder("TUNER_" + getFrequency()); + mRecorder = RecorderFactory.getBasebandRecorder("TUNER_" + getFrequency(), userPreferences); mRecorder.setSampleRate((float)getSampleRate()); mRecorder.start(); addBufferListener(mRecorder); diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java index 354983a29..3ec732e41 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerEditor.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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; @@ -26,7 +23,7 @@ import io.github.dsheirer.gui.control.JFrequencyControl; import io.github.dsheirer.gui.editor.Editor; import io.github.dsheirer.gui.editor.EmptyEditor; -import io.github.dsheirer.record.RecorderManager; +import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationEditor; import io.github.dsheirer.source.tuner.configuration.TunerConfigurationFactory; @@ -69,12 +66,12 @@ public class TunerEditor extends Editor private JScrollPane mEditorScroller; private Editor mEditor = new EmptyEditor<>("a tuner"); private JideSplitPane mEditorSplitPane = new JideSplitPane(); - private RecorderManager mRecorderManager; + private UserPreferences mRecorderManager; - public TunerEditor(TunerConfigurationModel tunerConfigurationModel, RecorderManager recorderManager) + public TunerEditor(TunerConfigurationModel tunerConfigurationModel, UserPreferences userPreferences) { mTunerConfigurationModel = tunerConfigurationModel; - mRecorderManager = recorderManager; + mRecorderManager = userPreferences; init(); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java index 4be42fa55..172750f8a 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerManager.java @@ -55,7 +55,6 @@ public class TunerManager private final static Logger mLog = LoggerFactory.getLogger(TunerManager.class); private static final int MAXIMUM_USB_2_DATA_RATE = 480000000; - private MixerManager mMixerManager; private TunerModel mTunerModel; private UserPreferences mUserPreferences; private Map> mUSBBusTunerMap = new TreeMap<>(); @@ -72,9 +71,8 @@ public class TunerManager LIBUSB_TRANSFER_PROCESSOR = new USBMasterProcessor(); } - public TunerManager(MixerManager mixerManager, TunerModel tunerModel, UserPreferences userPreferences) + public TunerManager(TunerModel tunerModel, UserPreferences userPreferences) { - mMixerManager = mixerManager; mTunerModel = tunerModel; mUserPreferences = userPreferences; @@ -563,8 +561,7 @@ private TunerInitStatus initRTL2832Tuner(TunerClass tunerClass, */ private MixerTunerDataLine getMixerTunerDataLine(TunerType tunerClass) { - Collection datalines = - mMixerManager.getMixerTunerDataLines(); + Collection datalines = MixerManager.getMixerTunerDataLines(); for(MixerTunerDataLine mixerTDL : datalines) { diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerViewPanel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerViewPanel.java index eecd08009..ffd5205cc 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerViewPanel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerViewPanel.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2020 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * ***************************************************************************** + * 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; @@ -25,7 +22,6 @@ import com.jidesoft.swing.JideSplitPane; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor; -import io.github.dsheirer.record.RecorderManager; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.tuner.TunerEvent.Event; import io.github.dsheirer.source.tuner.recording.AddRecordingTunerDialog; @@ -74,10 +70,10 @@ public class TunerViewPanel extends JPanel private TunerEditor mTunerEditor; private UserPreferences mUserPreferences; - public TunerViewPanel(TunerModel tunerModel, UserPreferences userPreferences, RecorderManager recorderManager) + public TunerViewPanel(TunerModel tunerModel, UserPreferences userPreferences) { mTunerModel = tunerModel; - mTunerEditor = new TunerEditor(mTunerModel.getTunerConfigurationModel(), recorderManager); + mTunerEditor = new TunerEditor(mTunerModel.getTunerConfigurationModel(), userPreferences); mUserPreferences = userPreferences; init();