Files
sdrtrunk/src/main/java/io/github/dsheirer/audio/AudioSegment.java
robekl 39f6d6c831 Performance improvements (#746)
* Misc minor performance improvements

* Use more concise loop syntax

* Use more concise Collection sort syntax

* Use StringUtils pad methods

* Optimize some loops and double-map-lookups

* Optimize array instantiation

There are two styles to convert a collection to an array: either
using a pre-sized array (like c.toArray(new String[c.size()])) or
using an empty array (like c.toArray(new String[0]).

In older Java versions using pre-sized array was recommended,
as the reflection call which is necessary to create an array
of proper size was quite slow. However since late updates of
OpenJDK 6 this call was intrinsified, making the performance
of the empty array version the same and sometimes even better,
compared to the pre-sized version. Also passing pre-sized array
is dangerous for a concurrent or synchronized collection as
a data race is possible between the size and toArray call
which may result in extra nulls at the end of the array,
if the collection was concurrently shrunk during the operation.

* Cleanup string concatenation

* Use EnumMap where possible

* Use FastMath instead of Math

* Initialize complex things only once

* Reduce method size to aloow more JIT inlining

* remove map of short->float

* Move field to local variable

* Cleanup Map lookup

Co-authored-by: Rob Ekl <xwq876@zebra.com>
2020-04-25 07:57:56 -04:00

434 lines
15 KiB
Java

/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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.Date;
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<IdentifierUpdateNotification>
{
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<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
private List<float[]> mAudioBuffers = new CopyOnWriteArrayList();
private AtomicInteger mConsumerCount = new AtomicInteger();
private AliasList mAliasList;
private long mStartTimestamp = System.currentTimeMillis();
private boolean mDisposing = false;
private AudioSegment mLinkedAudioSegment;
private int mTimeslot;
/**
* Constructs an instance
*
* @param aliasList for accessing aliases associated with identifiers for this audio segment.
*/
public AudioSegment(AliasList aliasList, int timeslot)
{
mAliasList = aliasList;
mTimeslot = timeslot;
mIdentifierCollection.setTimeslot(timeslot);
}
/**
* Timeslot for this audio segment
*/
public int getTimeslot()
{
return mTimeslot;
}
/**
* 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<BroadcastChannel> broadcastChannelsProperty()
{
return mBroadcastChannels;
}
/**
* Set of broadcast channels from identifier associated aliases for this segment.
*/
public Set<BroadcastChannel> 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;
}
/**
* Immutable 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<Identifier> identifiers)
{
for(Identifier identifier: identifiers)
{
addIdentifier(identifier);
}
}
/**
* Unmodifiable copy of the list of audio buffers for this segment.
*
* @return list of audio buffers
*/
public List<float[]> 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<IdentifierUpdateNotification> listener)
{
mIdentifierUpdateNotificationBroadcaster.addListener(listener);
}
/**
* Removes the identifier update listener.
*/
public void removeIdentifierUpdateNotificationListener(Listener<IdentifierUpdateNotification> listener)
{
mIdentifierUpdateNotificationBroadcaster.removeListener(listener);
}
/**
* Adds the identifier to this audio segment and updates record, priority and streaming properties.
*
* Note: identifiers pass to this method must be checked for timeslot match.
*/
public void addIdentifier(Identifier identifier)
{
mIdentifierCollection.update(identifier);
List<Alias> aliases = mAliasList.getAliases(identifier);
for(Alias alias: aliases)
{
if(alias.isRecordable())
{
mRecordAudio.set(true);
}
//Add all broadcast channels for the alias ... let the set handle duplication.
mBroadcastChannels.addAll(alias.getBroadcastChannels());
//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);
}
}
}
/**
* 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)
{
//Only process add updates that match this timeslot
if(identifierUpdateNotification.getTimeslot() == getTimeslot())
{
if(identifierUpdateNotification.isAdd() || identifierUpdateNotification.isSilentAdd())
{
addIdentifier(identifierUpdateNotification.getIdentifier());
}
mIdentifierUpdateNotificationBroadcaster.broadcast(identifierUpdateNotification);
}
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Audio Segment\n Timeslot:").append(getTimeslot()).append("\n");
sb.append(" Start Time:").append(new Date(getStartTimestamp()));
sb.append(" Linked:").append(isLinked()).append("\n");
sb.append(" Recordable:").append(recordAudioProperty().get()).append("\n");
sb.append("Do Not Monitor:").append(isDoNotMonitor()).append("\n");
sb.append(" Stream To:").append(getBroadcastChannels()).append("\n");
sb.append(" Identifiers:\n").append(getIdentifierCollection()).append("\n");
return sb.toString();
}
}