Skip to content

Commit

Permalink
#660 Updates audio processing subsystem to use audio segments. Update…
Browse files Browse the repository at this point in the history
…s audio playback, recording and streaming to use audio segments. Adds support to UserPreferences for sound card selection and audio tone insertion during playback. Adds record preference for WAVE or MP3. Resolves issues with metadata in recordings. (#675)
  • Loading branch information
DSheirer authored Mar 19, 2020
1 parent de249c4 commit fa16db1
Show file tree
Hide file tree
Showing 88 changed files with 4,434 additions and 4,171 deletions.
11 changes: 0 additions & 11 deletions .idea/compiler.xml

This file was deleted.

12 changes: 0 additions & 12 deletions .idea/libraries/imports.xml

This file was deleted.

3 changes: 2 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 0 additions & 12 deletions .idea/modules.xml

This file was deleted.

12 changes: 1 addition & 11 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
16 changes: 16 additions & 0 deletions copyright.template
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>
*****************************************************************************
15 changes: 1 addition & 14 deletions sdr-trunk.ipr
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<component name="DependencyValidationManager">
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</component>
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
Expand Down Expand Up @@ -60,21 +61,7 @@
<version value="1.0" />
</component>
<component name="JavadocGenerationManager">
<option name="OUTPUT_DIRECTORY" />
<option name="OPTION_SCOPE" value="protected" />
<option name="OPTION_HIERARCHY" value="true" />
<option name="OPTION_NAVIGATOR" value="true" />
<option name="OPTION_INDEX" value="true" />
<option name="OPTION_SEPARATE_INDEX" value="true" />
<option name="OPTION_DOCUMENT_TAG_USE" value="false" />
<option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false" />
<option name="OPTION_DOCUMENT_TAG_VERSION" value="false" />
<option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true" />
<option name="OPTION_DEPRECATED_LIST" value="true" />
<option name="OTHER_OPTIONS" value="" />
<option name="HEAP_SIZE" />
<option name="LOCALE" />
<option name="OPEN_IN_BROWSER" value="true" />
</component>
<component name="Palette2">
<group name="Swing">
Expand Down
22 changes: 14 additions & 8 deletions src/main/java/io/github/dsheirer/alias/AliasList.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<TalkgroupRange,Alias> 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() +
Expand Down Expand Up @@ -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);
Expand All @@ -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<RadioRange,Alias> 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() +
Expand Down
144 changes: 110 additions & 34 deletions src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReusableAudioPacket> mAudioPacketListener;
private Listener<AudioSegment> mAudioSegmentListener;
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> 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<IdentifierUpdateNotification> 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<ReusableAudioPacket> 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<IdentifierUpdateNotification> 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<ReusableAudioPacket> 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<AudioSegment> listener)
{
return mAudioPacketListener != null;
mAudioSegmentListener = listener;
}

/**
* Unregisters the audio segment listener from receiving audio segments from this module.
*/
@Override
public void removeAudioSegmentListener()
{
mAudioSegmentListener = null;
}
}
Loading

0 comments on commit fa16db1

Please sign in to comment.