From fa16db129bda8e91598b0d5bf96150e203a29df8 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Thu, 19 Mar 2020 10:35:05 +0000 Subject: [PATCH] #660 Updates audio processing subsystem to use audio segments. Updates audio playback, recording and streaming to use audio segments. Adds support to UserPreferences for sound card selection and audio tone insertion during playback. Adds record preference for WAVE or MP3. Resolves issues with metadata in recordings. (#675) --- .idea/compiler.xml | 11 - .idea/libraries/imports.xml | 12 - .idea/misc.xml | 3 +- .idea/modules.xml | 12 - .idea/vcs.xml | 12 +- build.gradle | 2 + copyright.template | 16 + sdr-trunk.ipr | 15 +- .../io/github/dsheirer/alias/AliasList.java | 22 +- .../dsheirer/audio/AbstractAudioModule.java | 144 ++++- .../audio/AudioMetadataProcessor.java | 85 --- .../io/github/dsheirer/audio/AudioModule.java | 89 +-- .../dsheirer/audio/AudioPacketManager.java | 98 --- .../github/dsheirer/audio/AudioSegment.java | 401 ++++++++++++ .../audio/AudioSegmentBroadcaster.java | 52 ++ .../io/github/dsheirer/audio/AudioUtils.java | 33 +- .../dsheirer/audio/IAudioPacketListener.java | 9 - .../dsheirer/audio/IAudioPacketProvider.java | 10 - .../dsheirer/audio/IAudioSegmentListener.java | 33 + .../dsheirer/audio/IAudioSegmentProvider.java | 39 ++ .../audio/broadcast/AudioRecording.java | 17 +- .../broadcast/AudioStreamingManager.java | 206 ++++++ .../audio/broadcast/BroadcastFactory.java | 19 - .../audio/broadcast/BroadcastModel.java | 78 +-- .../audio/broadcast/StreamManager.java | 247 -------- .../audio/codec/mbe/AmbeAudioModule.java | 5 +- .../audio/codec/mbe/ImbeAudioModule.java | 5 +- .../audio/codec/mbe/JmbeAudioModule.java | 15 +- .../codec/mbe/MBECallSequenceConverter.java | 130 ++-- .../audio/convert/IAudioConverter.java | 4 +- .../audio/convert/MP3AudioConverter.java | 33 +- .../audio/convert/MP3SilenceGenerator.java | 17 +- .../audio/convert/thumbdv/ThumbDv.java | 21 +- .../audio/playback/AudioChannelPanel.java | 6 + .../dsheirer/audio/playback/AudioOutput.java | 586 ++++++++++++------ .../dsheirer/audio/playback/AudioPanel.java | 41 +- .../audio/playback/AudioPlaybackManager.java | 552 +++++------------ .../audio/playback/MonoAudioOutput.java | 14 +- .../audio/playback/StereoAudioOutput.java | 14 +- .../state/AlwaysUnsquelchedDecoderState.java | 46 +- .../dsheirer/controller/ControllerPanel.java | 35 +- .../channel/ChannelProcessingManager.java | 29 +- .../dsheirer/gui/JavaFxWindowManager.java | 4 +- .../java/io/github/dsheirer/gui/SDRTrunk.java | 73 ++- .../preference/PreferenceEditorFactory.java | 21 +- .../gui/preference/PreferenceEditorType.java | 36 +- .../gui/preference/PreferencesEditor.java | 169 ++--- .../playback/PlaybackPreferenceEditor.java | 343 ++++++++++ .../preference/playback/ToneFrequency.java | 78 +++ .../gui/preference/playback/ToneUtil.java | 68 ++ .../gui/preference/playback/ToneVolume.java | 65 ++ .../record/RecordPreferenceEditor.java | 91 +++ .../tuner/TunerPreferenceEditor.java | 4 +- .../identifier/IdentifierCollection.java | 58 +- .../MutableIdentifierCollection.java | 4 - .../string/SimpleStringIdentifier.java | 42 ++ .../dsheirer/module/ProcessingChain.java | 72 +-- .../module/decode/DecoderFactory.java | 62 +- .../decode/p25/audio/P25P1AudioModule.java | 79 +-- .../decode/p25/audio/P25P2AudioModule.java | 69 +-- .../decode/p25/phase2/P25P2MessageFramer.java | 40 +- .../dsheirer/preference/PreferenceType.java | 31 +- .../dsheirer/preference/UserPreferences.java | 52 +- .../playback/PlaybackPreference.java | 288 +++++++++ .../preference/record/RecordPreference.java | 93 +++ .../github/dsheirer/record/AudioRecorder.java | 340 ---------- .../record/AudioRecordingManager.java | 281 +++++++++ .../dsheirer/record/AudioSegmentRecorder.java | 134 ++++ .../github/dsheirer/record/RecordFormat.java | 44 ++ .../dsheirer/record/RecorderFactory.java | 65 +- .../dsheirer/record/RecorderManager.java | 371 ----------- .../dsheirer/record/mp3/MP3Recorder.java | 99 --- ...veMetadataType.java => AudioMetadata.java} | 42 +- .../record/wave/AudioMetadataUtils.java | 358 +++++++++++ .../record/wave/AudioPacketWaveRecorder.java | 257 -------- .../dsheirer/record/wave/WaveMetadata.java | 425 ------------- .../dsheirer/record/wave/WaveWriter.java | 10 +- .../sample/buffer/ReusableAudioPacket.java | 275 -------- .../buffer/ReusableAudioPacketQueue.java | 77 --- .../sample/buffer/ReusableBufferQueue.java | 50 +- .../github/dsheirer/source/SourceManager.java | 11 +- .../mixer/MixerChannelConfiguration.java | 154 +++-- .../dsheirer/source/mixer/MixerManager.java | 159 +++-- .../source/mixer/MixerSourceEditor.java | 279 ++++----- .../source/tuner/TunerController.java | 36 +- .../dsheirer/source/tuner/TunerEditor.java | 37 +- .../dsheirer/source/tuner/TunerManager.java | 7 +- .../dsheirer/source/tuner/TunerViewPanel.java | 34 +- 88 files changed, 4434 insertions(+), 4171 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/libraries/imports.xml delete mode 100644 .idea/modules.xml create mode 100644 copyright.template delete mode 100644 src/main/java/io/github/dsheirer/audio/AudioMetadataProcessor.java delete mode 100644 src/main/java/io/github/dsheirer/audio/AudioPacketManager.java create mode 100644 src/main/java/io/github/dsheirer/audio/AudioSegment.java create mode 100644 src/main/java/io/github/dsheirer/audio/AudioSegmentBroadcaster.java delete mode 100644 src/main/java/io/github/dsheirer/audio/IAudioPacketListener.java delete mode 100644 src/main/java/io/github/dsheirer/audio/IAudioPacketProvider.java create mode 100644 src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java create mode 100644 src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java create mode 100644 src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java delete mode 100644 src/main/java/io/github/dsheirer/audio/broadcast/StreamManager.java create mode 100644 src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java create mode 100644 src/main/java/io/github/dsheirer/gui/preference/playback/ToneFrequency.java create mode 100644 src/main/java/io/github/dsheirer/gui/preference/playback/ToneUtil.java create mode 100644 src/main/java/io/github/dsheirer/gui/preference/playback/ToneVolume.java create mode 100644 src/main/java/io/github/dsheirer/gui/preference/record/RecordPreferenceEditor.java create mode 100644 src/main/java/io/github/dsheirer/identifier/string/SimpleStringIdentifier.java create mode 100644 src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java create mode 100644 src/main/java/io/github/dsheirer/preference/record/RecordPreference.java delete mode 100644 src/main/java/io/github/dsheirer/record/AudioRecorder.java create mode 100644 src/main/java/io/github/dsheirer/record/AudioRecordingManager.java create mode 100644 src/main/java/io/github/dsheirer/record/AudioSegmentRecorder.java create mode 100644 src/main/java/io/github/dsheirer/record/RecordFormat.java delete mode 100644 src/main/java/io/github/dsheirer/record/RecorderManager.java delete mode 100644 src/main/java/io/github/dsheirer/record/mp3/MP3Recorder.java rename src/main/java/io/github/dsheirer/record/wave/{WaveMetadataType.java => AudioMetadata.java} (58%) create mode 100644 src/main/java/io/github/dsheirer/record/wave/AudioMetadataUtils.java delete mode 100644 src/main/java/io/github/dsheirer/record/wave/AudioPacketWaveRecorder.java delete mode 100644 src/main/java/io/github/dsheirer/record/wave/WaveMetadata.java delete mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacket.java delete mode 100644 src/main/java/io/github/dsheirer/sample/buffer/ReusableAudioPacketQueue.java 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();