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 @@
+
@@ -60,21 +61,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 extends Boolean> 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 extends Boolean> 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 extends RecordFormat> 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 extends Boolean> 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