Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,22 @@
*/
package io.github.dsheirer.audio.broadcast;

import io.github.dsheirer.audio.convert.AudioFrames;
import io.github.dsheirer.audio.convert.ISilenceGenerator;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3FrameTools;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.util.ThreadPool;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration> extends AbstractAudioBroadcaster<T>
{
Expand All @@ -48,6 +46,7 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ISilenceGenerator mSilenceGenerator;

private BroadcastFormat mBroadcastFormat;
private long mDelay;
private long mMaximumRecordingAge;
private AtomicBoolean mStreaming = new AtomicBoolean();
Expand All @@ -56,6 +55,8 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
protected int mInlineInterval;
protected int mInlineRemaining = -1;

private int mTimeOverrun = 0;

/**
* AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
* generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
Expand All @@ -80,10 +81,11 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
public AudioStreamingBroadcaster(T broadcastConfiguration, InputAudioFormat inputAudioFormat, MP3Setting mp3Setting)
{
super(broadcastConfiguration);
mBroadcastFormat = broadcastConfiguration.getBroadcastFormat();
mDelay = getBroadcastConfiguration().getDelay();
mMaximumRecordingAge = getBroadcastConfiguration().getMaximumRecordingAge();
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat(),
inputAudioFormat, mp3Setting);
inputAudioFormat, mp3Setting);
}

public void dispose()
Expand Down Expand Up @@ -249,7 +251,6 @@ protected boolean connected()
public boolean canConnect()
{
BroadcastState state = getBroadcastState();

return state != BroadcastState.CONNECTED && !state.isErrorState();
}

Expand All @@ -270,10 +271,8 @@ protected boolean isErrorState()
public class RecordingQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private ByteArrayInputStream mInputStream;
private AudioFrames mInputFrames;
private IdentifierCollection mInputIdentifierCollection;
private long mFinalSilencePadding = 0;
private int mBytesToStreamPerInterval = 0;

@Override
public void run()
Expand All @@ -282,48 +281,35 @@ public void run()
{
try
{
if(mInputStream == null || mInputStream.available() <= 0)
int timeSent = 0;

if(mInputFrames == null || !mInputFrames.hasNextFrame())
{
nextRecording();
}

if(mInputStream != null)
if(mInputFrames != null && mInputFrames.hasNextFrame())
{
int length = FastMath.min(mBytesToStreamPerInterval, mInputStream.available());

byte[] audio = new byte[length];

try
{
int read = mInputStream.read(audio);
broadcastAudio(audio, mInputIdentifierCollection);
}
catch(IOException ioe)
{
mLog.error("Error reading from in-memory audio recording input stream", ioe);
}

//If this is the final frame fragment then append silence padding to fill the final interval segment
if(length < mBytesToStreamPerInterval && mFinalSilencePadding > 0)
while(mInputFrames.hasNextFrame() && timeSent < PROCESSOR_RUN_INTERVAL_MS)
{
List<byte[]> finalSilence = mSilenceGenerator.generate(mFinalSilencePadding);

for(byte[] silence: finalSilence)
{
broadcastAudio(silence, null);
}

mFinalSilencePadding = 0;
mInputFrames.nextFrame();
broadcastAudio(mInputFrames.getCurrentFrame(), mInputIdentifierCollection);
timeSent += mInputFrames.getCurrentFrameDuration();
}
}
else

if((mInputFrames == null || !mInputFrames.hasNextFrame()) && timeSent < PROCESSOR_RUN_INTERVAL_MS)
{
List<byte[]> silenceFrames = mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS);
for(byte[] silence: silenceFrames)
AudioFrames silenceFrames = mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS - mTimeOverrun - timeSent);
while(silenceFrames.hasNextFrame())
{
broadcastAudio(silence, null);
silenceFrames.nextFrame();
broadcastAudio(silenceFrames.getCurrentFrame(), null);
timeSent += silenceFrames.getCurrentFrameDuration();
}
}

mTimeOverrun += timeSent - PROCESSOR_RUN_INTERVAL_MS;
}
catch(Throwable t)
{
Expand All @@ -341,15 +327,15 @@ private void nextRecording()
{
boolean metadataUpdateRequired = false;

if(mInputStream != null)
if(mInputFrames != null)
{
mStreamedAudioCount++;
broadcast(new BroadcastEvent(AudioStreamingBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
metadataUpdateRequired = true;
}

mInputStream = null;
mInputFrames = null;
mInputIdentifierCollection = null;

//Peek at the next recording but don't remove it from the queue yet, so we can inspect the start time for
Expand Down Expand Up @@ -380,20 +366,15 @@ private void nextRecording()

if(audio.length > 0)
{
//Calculate how many bytes we'll send with each processor run interval
double intervals = (double)nextRecording.getRecordingLength() / (double)PROCESSOR_RUN_INTERVAL_MS;
mBytesToStreamPerInterval = (int)((double)audio.length / intervals);

mInputStream = new ByteArrayInputStream(audio);
mInputIdentifierCollection = nextRecording.getIdentifierCollection();

mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
(nextRecording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);

while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
switch(mBroadcastFormat)
{
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
case MP3:
mInputFrames = MP3FrameTools.split(audio);
break;
default:
throw new IllegalArgumentException("Unsupported broadcast format [" + mBroadcastFormat + "]");
}
mInputIdentifierCollection = nextRecording.getIdentifierCollection();

if(connected())
{
Expand All @@ -409,7 +390,7 @@ private void nextRecording()
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
"stream recording [" + nextRecording.getPath().toString() + "] - skipping recording - ", ioe);

mInputStream = null;
mInputFrames = null;
mInputIdentifierCollection = null;
metadataUpdateRequired = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
Expand All @@ -48,6 +41,12 @@
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.table.AbstractTableModel;

public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
{
Expand All @@ -58,7 +57,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio

private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";

public static final int COLUMN_SERVER_ICON = 0;
public static final int COLUMN_BROADCAST_SERVER_TYPE = 0;
public static final int COLUMN_STREAM_NAME = 1;
public static final int COLUMN_BROADCASTER_STATUS = 2;
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
Expand All @@ -67,7 +66,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;

public static final String[] COLUMN_NAMES = new String[]
{"Streaming", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};

private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
Expand Down Expand Up @@ -575,15 +574,8 @@ public Object getValueAt(int rowIndex, int columnIndex)
{
switch(columnIndex)
{
case COLUMN_SERVER_ICON:
String iconPath = configuredBroadcast.getBroadcastConfiguration()
.getBroadcastServerType().getIconPath();

if(iconPath != null && mIconModel != null)
{
return mIconModel.getScaledIcon(iconPath, 14);
}
break;
case COLUMN_BROADCAST_SERVER_TYPE:
return configuredBroadcast.getBroadcastServerType();
case COLUMN_STREAM_NAME:
return configuredBroadcast.getBroadcastConfiguration().getName();
case COLUMN_BROADCASTER_STATUS:
Expand Down Expand Up @@ -653,8 +645,8 @@ public Class<?> getColumnClass(int columnIndex)
case COLUMN_BROADCASTER_STREAMED_COUNT:
case COLUMN_BROADCASTER_ERROR_COUNT:
return Integer.class;
case COLUMN_SERVER_ICON:
return ImageIcon.class;
case COLUMN_BROADCAST_SERVER_TYPE:
return BroadcastServerType.class;
case COLUMN_STREAM_NAME:
default:
return String.class;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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
Expand All @@ -14,26 +14,26 @@
*
* 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.broadcast;

public enum BroadcastServerType
{
/**
* Broadcastify feeds (ie streaming) service
*/
BROADCASTIFY("Broadcastify Feed", "/images/broadcastify.png"), //Icecast Server 2.3.2
BROADCASTIFY("Broadcastify Feed", "images/broadcastify.png"), //Icecast Server 2.3.2

/**
* Broadcastify calls - completed audio recording push service
*/
BROADCASTIFY_CALL("Broadcastify Call", "/images/broadcastify.png"),
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),

ICECAST_HTTP("Icecast 2 (v2.4+)", "/images/icecast.png"),
ICECAST_TCP("Icecast (v2.3)", "/images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "/images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "/images/shoutcast.png"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
UNKNOWN("Unknown", null);

private String mLabel;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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
Expand All @@ -14,8 +14,8 @@
*
* 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.broadcast;

public enum BroadcastState
Expand Down Expand Up @@ -68,7 +68,7 @@ public enum BroadcastState
/**
* Specified mount point is already in use
*/
MOUNT_POINT_IN_USE("Mount Point In Use", false),
MOUNT_POINT_IN_USE("Mount Point In Use", true),

/**
* Network is unavailable or the server address cannot be resolved
Expand Down Expand Up @@ -103,10 +103,10 @@ public enum BroadcastState
private String mLabel;
private boolean mErrorState;

private BroadcastState(String label, boolean connected)
BroadcastState(String label, boolean errorState)
{
mLabel = label;
mErrorState = connected;
mErrorState = errorState;
}

public String toString()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2022 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
Expand All @@ -15,23 +14,29 @@
*
* 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.broadcast;

import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
import java.awt.Color;
import java.awt.Component;
import net.miginfocom.swing.MigLayout;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.Color;
import java.awt.Component;

/**
* Table of broadcast streams and statuses.
*/
public class BroadcastStatusPanel extends JPanel
{
private JTable mTable;
Expand All @@ -41,6 +46,12 @@ public class BroadcastStatusPanel extends JPanel
private UserPreferences mUserPreferences;
private String mPreferenceKey;

/**
* Constructs an instance
* @param broadcastModel to access the streams
* @param userPreferences for configuring the panel
* @param preferenceKey to store column preferences for this panel.
*/
public BroadcastStatusPanel(BroadcastModel broadcastModel, UserPreferences userPreferences, String preferenceKey)
{
mBroadcastModel = broadcastModel;
Expand All @@ -65,13 +76,45 @@ private void init()
renderer.setHorizontalAlignment(SwingConstants.CENTER);

mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS).setCellRenderer(new StatusCellRenderer());
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCAST_SERVER_TYPE).setCellRenderer(new ServerTypeRenderer());
mColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, mPreferenceKey);

mScrollPane = new JScrollPane(mTable);

add(mScrollPane);
}

public class ServerTypeRenderer extends DefaultTableCellRenderer
{
public ServerTypeRenderer()
{
setOpaque(true);
setHorizontalAlignment(SwingConstants.CENTER);
}

@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
JLabel component = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

if(value instanceof BroadcastServerType broadcastServerType)
{
component.setText(broadcastServerType.toString());
Icon icon = new Icon("empty", broadcastServerType.getIconPath());
ImageIcon imageIcon = icon.getIcon();
ImageIcon scaledIcon = IconModel.getScaledIcon(imageIcon, 13);
component.setIcon(scaledIcon);
}
else
{
component.setText(null);
component.setIcon(null);
}

return component;
}
}

/**
* Custom cell renderer for the broadcast state column.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,20 @@

import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.broadcast.icecast.IcecastMetadata;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
Expand All @@ -42,13 +49,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
Expand Down Expand Up @@ -143,8 +143,9 @@ private boolean connect()
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);

// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(IcecastHTTPAudioBroadcaster.class));
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);

mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
mSocketConnector.setHandler(new IcecastHTTPIOHandler());
Expand Down Expand Up @@ -315,10 +316,49 @@ public void exceptionCaught(IoSession session, Throwable throwable) throws Excep
* intercept the out of bounds exception and then inspect the message hex dump to see if it's an
* OK response and then play like everything is well and good.
*/
if(cause instanceof ArrayIndexOutOfBoundsException &&
((ProtocolDecoderException)throwable).getHexdump().startsWith(HTTP_1_0_OK_HEX_DUMP))
if(cause instanceof ArrayIndexOutOfBoundsException)
{
setBroadcastState(BroadcastState.CONNECTED);
String hexDump = ((ProtocolDecoderException)throwable).getHexdump();

if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
HttpDumpMessage message = new HttpDumpMessage(hexDump);

if(message.hasHttpResponseCode())
{
switch(message.getHttpResponseCode())
{
case 403: //Forbidden
if(message.toString().contains("Mountpoint in use"))
{
mLog.error("Stream [" + getStreamName() + "] - unable to connect - mountpoint in use");
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
disconnect();
}
else
{
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
break;
default:
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
}
else
{
mLog.error("HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
}
}
else
{
Expand Down Expand Up @@ -374,4 +414,97 @@ public void messageReceived(IoSession session, Object object) throws Exception
}
}
}

/**
* Class for parsing HTTP response messages from Icecast 2.4.2+
*/
public class HttpDumpMessage
{
private String mHexDump;
private String mMessage;
private int mHttpResponseCode = -1;

public HttpDumpMessage(String hexDump)
{
mHexDump = hexDump;

String[] split = mHexDump.split(" ");
byte[] bytes = new byte[split.length];

int pointer = 0;
for(String a : split)
{
try
{
int value = Integer.parseInt(a, 16);
bytes[pointer++] = (byte) (value & 0xFF);
}
catch(Exception e)
{
pointer++;
}
}

mMessage = new String(bytes);

Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
Matcher m = pattern.matcher(mMessage);

if(m.find())
{
try
{
mHttpResponseCode = Integer.parseInt(m.group(1));
}
catch(Exception e)
{
mLog.error("Unable to parse HTTP response code that was matched from message: " + m.group(1));
}
}
}

/**
* Indicates if an HTTP response code was parsed from the message
* @return true if it was parsed.
*/
public boolean hasHttpResponseCode()
{
return mHttpResponseCode != -1;
}

/**
* Response code parsed from the hexdump message
* @return value.
*/
public int getHttpResponseCode()
{
return mHttpResponseCode;
}

@Override
public String toString()
{
return mMessage;
}
}

public static void main(String[] args)
{
String text = "HTTP/1.0 403 Forbidden";

Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");

Matcher m = pattern.matcher(text);

if(m.find())
{
mLog.info("Matching Group Count: " + m.groupCount());
mLog.info("Match 0: " + m.group(0));
mLog.info("Match 1: " + m.group(1));
}
else
{
mLog.info("No Match");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
import io.github.dsheirer.audio.broadcast.icecast.IcecastMetadata;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.UnresolvedAddressException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
Expand All @@ -37,14 +43,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.UnresolvedAddressException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class);
Expand Down Expand Up @@ -215,7 +213,13 @@ public void disconnect()
}
else
{
setBroadcastState(BroadcastState.DISCONNECTED);
//Only set broadcast state to disconnected if it's not already in an error state, to prevent a restart. We
//want to preserve the error state that got us here, so the user can see it.
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}

mLastConnectionAttempt = System.currentTimeMillis();
}
}
Expand Down
133 changes: 133 additions & 0 deletions src/main/java/io/github/dsheirer/audio/convert/AudioFrames.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.convert;

import java.util.List;
import org.apache.commons.math3.util.FastMath;

public abstract class AudioFrames
{
protected final int audioDuration;
protected final List<byte[]> audioFrames;
protected int mCurrentFrame = -1;

/**
* Create new AudioFrame
* @param audioDuration total duration in milliseconds
* @param audioFrames series of audio frames, split on frame boundaries
*/
public AudioFrames(int audioDuration, List<byte[]> audioFrames)
{
this.audioDuration = audioDuration;
this.audioFrames = audioFrames;
}

/**
* Get total duration of frames
* @return total duration in milliseconds
*/
public int getDuration()
{
return audioDuration;
}

/**
* Get all frames
* @return list of byte arrays, with each element representing a single frame of audio
*/
public List<byte[]> getFrames()
{
return audioFrames;
}

/**
* Get current frame
* @return byte array with the current frame of audio
*/
public byte[] getCurrentFrame()
{
return audioFrames.get(mCurrentFrame);
}

/**
* Get current frame duration
* @return frame duration in milliseconds
*/
public abstract int getCurrentFrameDuration();

/**
* Indicates if there is a frame before the current one
* @return true if the frame exists, otherwise false
*/
public boolean hasPrevFrame()
{
int prevFrame = mCurrentFrame - 1;
return prevFrame >= 0 && prevFrame < audioFrames.size();
}

/**
* Indicates if there is a frame after the current one
* @return true if the frame exists, otherwise false
*/
public boolean hasNextFrame()
{
int nextFrame = mCurrentFrame + 1;
return nextFrame >= 0 && nextFrame < audioFrames.size();
}

/**
* Seek to the previous frame
*/
public void prevFrame()
{
mCurrentFrame -= 1;
}

/**
* Seek to the next frame
*/
public void nextFrame()
{
mCurrentFrame += 1;
}

/**
* Restart from beginning
* Must call nextFrame() to get first frame
*/
public void restart()
{
mCurrentFrame = -1;
}

/**
* Seek by the specified time
* @param duration_ms time in milliseconds, positive or negative
*/
public void seek(int duration_ms)
{
int step = duration_ms < 0 ? -1 : 1;
int actual_ms = 0;
while(FastMath.abs(actual_ms) < FastMath.abs(duration_ms))
{
mCurrentFrame += step;
actual_ms += getCurrentFrameDuration();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@

public interface ISilenceGenerator
{
List<byte[]> generate(long duration);
AudioFrames generate(long duration);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package io.github.dsheirer.audio.convert;

import io.github.dsheirer.audio.AudioFormats;
import java.util.EnumSet;

import javax.sound.sampled.AudioFormat;

Expand All @@ -29,8 +30,8 @@
public enum InputAudioFormat
{
SR_8000(AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO, "16-Bit 8000 Hz (no resample)"),
SR_16000(AudioFormats.PCM_SIGNED_16000_HZ_16_BIT_MONO, "16-Bit 16000 Hz"),
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16_BIT_MONO, "16-Bit 22050 Hz (default)"),
SR_16000(AudioFormats.PCM_SIGNED_16000_HZ_16_BIT_MONO, "16-Bit 16000 Hz (default)"),
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16_BIT_MONO, "16-Bit 22050 Hz"),
SR_44100(AudioFormats.PCM_SIGNED_44100_HZ_16_BIT_MONO, "16-Bit 44100 Hz"),

SR_32_8000(AudioFormats.PCM_SIGNED_8000_HZ_32_BIT_MONO, "32-Bit 8000 Hz (no resample)"),
Expand All @@ -41,6 +42,17 @@ public enum InputAudioFormat
private AudioFormat mAudioFormat;
private String mLabel;

/**
* Set that includes sample rates of 8 or 16 kHz
*/
public static final EnumSet<InputAudioFormat> SAMPLE_RATES_8_16 = EnumSet.of(SR_8000, SR_32_8000, SR_16000, SR_32_16000);

/**
* Set that includes sample rates of 8, 16, or 22 kHz
*/
public static final EnumSet<InputAudioFormat> SAMPLE_RATES_8_16_22 = EnumSet.of(SR_8000, SR_32_8000, SR_16000,
SR_32_16000, SR_22050, SR_32_22050);

/**
* Constructs an instance
* @param audioFormat for the specified sample rate
Expand All @@ -56,7 +68,7 @@ public enum InputAudioFormat
*/
public static InputAudioFormat getDefault()
{
return SR_22050;
return SR_16000;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,17 @@ public MP3AudioConverter(InputAudioFormat inputAudioFormat, MP3Setting setting,
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
mNormalizeAudio = normalizeAudio;

//Ensure input audio sample rate is supported for the MP3 setting and update as necessary - should never happen.
if((mInputAudioFormat.getSampleRate() - mEncoder.getEffectiveSampleRate()) > 1.0)
{
mInputAudioFormat = InputAudioFormat.getDefault();
mLog.warn("MP3 setting [" + setting + "] does not support requested input audio sample rate [" +
inputAudioFormat + "] - using default sample rate [" + mInputAudioFormat + "]");
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
}

//Resampling is only required if desired input sample rate is not system default of 8kHz
if(inputAudioFormat != InputAudioFormat.SR_8000 && inputAudioFormat != InputAudioFormat.SR_32_8000)
if(mInputAudioFormat != InputAudioFormat.SR_8000 && mInputAudioFormat != InputAudioFormat.SR_32_8000)
{
mResampler = LameFactory.getResampler(inputAudioFormat);
}
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/io/github/dsheirer/audio/convert/MP3AudioFrames.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.convert;

import java.util.List;

public class MP3AudioFrames extends AudioFrames
{

/**
* Create new MP3AudioFrame
* @param audioDuration total duration in milliseconds
* @param audioFrames series of audio frames, split on frame boundaries
*/
public MP3AudioFrames(int audioDuration, List<byte[]> audioFrames)
{
super(audioDuration, audioFrames);
}

/**
* Get current frame duration
* @return frame duration in milliseconds
*/
public int getCurrentFrameDuration()
{
return MP3Header.getFrameDuration(getCurrentFrame(), 0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static void inspect(List<byte[]> frames)
}
}

/* TODO: Rework for MP3SilenceGenerator returning IAudioFrames
public static void main(String[] args)
{
mLog.info("Starting ...");
Expand All @@ -66,4 +67,5 @@ public static void main(String[] args)
mLog.info("Finished");
}
*/
}
59 changes: 59 additions & 0 deletions src/main/java/io/github/dsheirer/audio/convert/MP3FrameTools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.convert;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.util.FastMath;

public class MP3FrameTools {

private MP3FrameTools() {}

/**
* Split MP3 audio at frame boundaries
* @param input byte array of audio
* @return list of byte arrays containing audio frames
*/
public static MP3AudioFrames split(byte[] input)
{
List<byte[]> mp3Frames = new ArrayList<>();
int audioDuration = 0;

int offset = 0;
while(offset < input.length)
{
if(MP3Header.hasSync(input, offset))
{
int framelen = MP3Header.getFrameLength(input, offset);
mp3Frames.add(Arrays.copyOfRange(input, offset, offset + FastMath.min(framelen, input.length - offset)));
audioDuration += MP3Header.getFrameDuration(input, offset);
offset += framelen;
}
else
{
offset += 1;
}
}

return new MP3AudioFrames(audioDuration, mp3Frames);
}

}
69 changes: 69 additions & 0 deletions src/main/java/io/github/dsheirer/audio/convert/MP3Header.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

package io.github.dsheirer.audio.convert;

import io.github.dsheirer.audio.convert.MPEGLayer;
import io.github.dsheirer.audio.convert.MPEGVersion;
import org.apache.commons.math3.util.FastMath;

/**
* Utility class for inspecting/parsing MP3 frames
*/
Expand Down Expand Up @@ -88,6 +92,71 @@ public static ChannelMode getChannelMode(byte[] frame, int offset)
return ChannelMode.fromValue((frame[offset + 3] & 0xC0) >> 6);
}

/**
* Indicates if the frame has padding
* @param frame
* @param offset
* @return boolean
*/
public static boolean isPadded(byte[] frame, int offset)
{
return (frame[offset + 2] & 0x02) == 0x02;
}

/**
* Get the number of bytes in the frame
* @param frame
* @param offset
* @return frame length in bytes
*/
public static int getFrameLength(byte[] frame, int offset)
{
return (getFrameSamples(frame, offset) / 8) * (getBitRate(frame, offset) * 1000) / getSampleRate(frame, offset) + (isPadded(frame, offset) ? 1 : 0);
}

/**
* Get the number of samples in the frame
* @param frame
* @param offset
* @return length of frame in samples
*/
public static int getFrameSamples(byte[] frame, int offset)
{
switch(getMPEGVersion(frame, offset))
{
case V_1:
switch(getMPEGLayer(frame, offset))
{
case LAYER1: return 384;
case LAYER2: return 1152;
case LAYER3: return 1152;
default:
}
case V_2:
case V_2_5:
switch(getMPEGLayer(frame, offset))
{
case LAYER1: return 384;
case LAYER2: return 1152;
case LAYER3: return 576;
default:
}
default:
}
return 0;
}

/**
* Get the frame duration in milliseconds
* @param frame
* @param offset
* @return duration in milliseconds
*/
public static int getFrameDuration(byte[] frame, int offset)
{
return (int)FastMath.round((double)getFrameSamples(frame, offset) / (double)getSampleRate(frame, offset) * 1000);
}

/**
* Inspects the frame bytes and provides details about the header.
*/
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/io/github/dsheirer/audio/convert/MP3Setting.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package io.github.dsheirer.audio.convert;

import java.util.Arrays;
import java.util.List;
import net.sourceforge.lame.mp3.Lame;

/**
Expand Down Expand Up @@ -79,6 +81,30 @@ public int getSetting()
return mSetting;
}

/**
* Input audio formats that are supported by the MP3Setting value.
* @return list of supported input audio sample rates.
*/
public List<InputAudioFormat> getSupportedSampleRates()
{
switch(this)
{
case CBR_16:
{
return InputAudioFormat.SAMPLE_RATES_8_16.stream().toList();
}
case CBR_32:
case VBR_7:
{
return InputAudioFormat.SAMPLE_RATES_8_16_22.stream().toList();
}
default:
{
return Arrays.stream(InputAudioFormat.values()).toList();
}
}
}

@Override
public String toString()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@
package io.github.dsheirer.audio.convert;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.math3.util.FastMath;

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 mMP3AudioConverter;
private InputAudioFormat mInputAudioFormat;
private byte[] mPreviousPartialFrameData;
private byte[] mSilenceFrame;
private int mSilenceFrameDuration;

/**
* Generates MP3 audio silence frames
Expand All @@ -40,19 +41,17 @@ public MP3SilenceGenerator(InputAudioFormat inputAudioFormat, MP3Setting setting
{
mInputAudioFormat = inputAudioFormat;
mMP3AudioConverter = new MP3AudioConverter(inputAudioFormat, setting, false);
generate_frame();
}

/**
* Generates silence frames
* @param duration_ms in milliseconds
* Generates a single silence frame
* @return
*/
public List<byte[]> generate(long duration_ms)
private void generate_frame()
{
double duration_secs = (double)duration_ms / 1000.0;

//We generate silence at 8000 kHz, because it gets resampled by the silence generator to the target rate
int length = (int)(duration_secs * InputAudioFormat.SR_8000.getSampleRate());
int length = (int)(InputAudioFormat.SR_8000.getSampleRate());

List<float[]> silenceBuffers = new ArrayList<>();

Expand All @@ -64,7 +63,40 @@ public List<byte[]> generate(long duration_ms)
added += chunk;
}

return mMP3AudioConverter.convert(silenceBuffers);
List<byte[]> mp3Audio = mMP3AudioConverter.convert(silenceBuffers);
byte[] silence = new byte[0];
for(byte[] mp3Chunk: mp3Audio)
{
silence = merge(silence, mp3Chunk);
}

MP3AudioFrames mp3Frames = MP3FrameTools.split(silence);
if(mp3Frames.hasNextFrame())
{
mp3Frames.nextFrame();
mSilenceFrame = mp3Frames.getCurrentFrame();
mSilenceFrameDuration = mp3Frames.getCurrentFrameDuration();
}

}

/**
* Generates silence frames
* @param duration_ms in milliseconds
* @return
*/
public MP3AudioFrames generate(long duration_ms)
{
List<byte[]> silenceFrames = new ArrayList<>();
int silenceDuration = 0;

while(silenceDuration < duration_ms)
{
silenceFrames.add(mSilenceFrame);
silenceDuration += mSilenceFrameDuration;
}

return new MP3AudioFrames(silenceDuration, silenceFrames);
}

private static byte[] merge(byte[] a, byte[] b)
Expand All @@ -89,6 +121,7 @@ else if(b == null)
return c;
}

/* TODO: Rework for MP3SilenceGenerator returning IAudioFrames
public static void main(String[] args)
{
mLog.debug("Starting ...");
Expand All @@ -113,4 +146,5 @@ public static void main(String[] args)
mLog.debug("Finished");
}
*/
}
70 changes: 70 additions & 0 deletions src/main/java/io/github/dsheirer/buffer/AbstractNativeBuffer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.buffer;

/**
* Base native buffer class.
*/
public abstract class AbstractNativeBuffer implements INativeBuffer
{
private long mTimestamp;
private float mSamplesPerMillisecond;

/**
* Constructs an instance
* @param timestamp for the start of this buffer
* @param samplesPerMillisecond to calculate the time offset of sub-buffers extracted from this native buffer.
*/
public AbstractNativeBuffer(long timestamp, float samplesPerMillisecond)
{
mTimestamp = timestamp;
mSamplesPerMillisecond = samplesPerMillisecond;
}

/**
* Timestamp for the start of this buffer
* @return timestamp in milliseconds
*/
@Override
public long getTimestamp()
{
return mTimestamp;
}

/**
* Quantity of samples representing one millisecond of sample data, used for calculating fragment timestamp offsets.
* @return samples per millisecond count.
*/
public float getSamplesPerMillisecond()
{
return mSamplesPerMillisecond;
}

/**
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
* native buffer starting timestamp.
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
* @return timestamp adjusted to the fragment or sub-buffer start sample.
*/
protected long getFragmentTimestamp(int samplesPointer)
{
return getTimestamp() + (long)(samplesPointer / 2 / getSamplesPerMillisecond());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.buffer;

/**
* Base class for native buffer factories.
*/
public abstract class AbstractNativeBufferFactory implements INativeBufferFactory
{
private float mSamplesPerMillisecond = 0.0f;

@Override
public void setSamplesPerMillisecond(float samplesPerMillisecond)
{
mSamplesPerMillisecond = samplesPerMillisecond;
}

/**
* Quantity of I/Q sample pairs per milli-second at the current sample rate to use in calculating an accurate
* timestamp for sub-buffer that are generated from the native buffer.
* @return samples per millisecond.
*/
public float getSamplesPerMillisecond()
{
return mSamplesPerMillisecond;
}
}
23 changes: 10 additions & 13 deletions src/main/java/io/github/dsheirer/buffer/ByteNativeBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
* non-interleaved complex sample buffers converted from the raw byte sample array.
*/
public class ByteNativeBuffer implements INativeBuffer
public class ByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private final static float[] LOOKUP_VALUES;
Expand All @@ -46,24 +46,24 @@ public class ByteNativeBuffer implements INativeBuffer
}

private byte[] mSamples;
private long mTimestamp;

/**
* Constructs an instance
* @param samples to process
* @param timestamp of the samples
* @param averageDc measured from sample stream
* @param samplesPerMillisecond to calculate derivative timestamps for sub-buffers.
*/
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc)
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % FRAGMENT_SIZE != 0)
{
throw new IllegalArgumentException("Samples byte[] length [" + samples.length + "] must be an even multiple of " + FRAGMENT_SIZE);
}

mSamples = samples;
mTimestamp = timestamp;
mAverageDc = averageDc;
}

Expand All @@ -73,12 +73,6 @@ public int sampleCount()
return mSamples.length / 2;
}

@Override
public long getTimestamp()
{
return mTimestamp;
}

@Override
public Iterator<ComplexSamples> iterator()
{
Expand All @@ -104,6 +98,8 @@ public boolean hasNext()
@Override
public ComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);

float[] i = new float[FRAGMENT_SIZE];
float[] q = new float[FRAGMENT_SIZE];
int samplesOffset = mSamplesPointer;
Expand All @@ -115,8 +111,7 @@ public ComplexSamples next()
}

mSamplesPointer = samplesOffset;

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

Expand All @@ -136,6 +131,8 @@ public boolean hasNext()
@Override
public InterleavedComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);

float[] converted = new float[FRAGMENT_SIZE * 2];

int samplesPointer = mSamplesPointer;
Expand All @@ -147,7 +144,7 @@ public InterleavedComplexSamples next()

mSamplesPointer = samplesPointer;

return new InterleavedComplexSamples(converted, mTimestamp);
return new InterleavedComplexSamples(converted, timestamp);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
/**
* Implements a factory for creating ByteNativeBuffer instances
*/
public class ByteNativeBufferFactory implements INativeBufferFactory
public class ByteNativeBufferFactory extends AbstractNativeBufferFactory
{
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();

Expand All @@ -39,7 +39,7 @@ public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
calculateDc(copy);
}

return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc());
return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc(), getSamplesPerMillisecond());
}

/**
Expand Down
35 changes: 9 additions & 26 deletions src/main/java/io/github/dsheirer/buffer/FloatNativeBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,34 @@
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;

import java.util.Iterator;

/**
* Simple native buffer implementation that simply wraps a single, existing, non-native sample buffer.
*/
public class FloatNativeBuffer implements INativeBuffer
public class FloatNativeBuffer extends AbstractNativeBuffer
{
private float[] mInterleavedComplexSamples;
private long mTimestamp;

/**
* Constructs an instance
* @param complexSamples to wrap
* @param complexSamples interleaved to wrap
* @param timestamp for the buffer
*/
public FloatNativeBuffer(float[] complexSamples, long timestamp)
public FloatNativeBuffer(float[] complexSamples, long timestamp, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
mInterleavedComplexSamples = complexSamples;
mTimestamp = timestamp;
}

/**
* Constructs an instance
* @param complexSamples to wrap
*/
public FloatNativeBuffer(float[] complexSamples)
{
this(complexSamples, System.currentTimeMillis());
}

/**
* Constructs an instance
* @param samples to wrap
* @param timestamp for the buffer
*/
public FloatNativeBuffer(ComplexSamples samples, long timestamp)
public FloatNativeBuffer(ComplexSamples samples)
{
this(SampleUtils.interleave(samples), timestamp);
this(SampleUtils.interleave(samples), samples.timestamp(), 0.0f);
}

/**
Expand All @@ -69,7 +58,7 @@ public FloatNativeBuffer(ComplexSamples samples, long timestamp)
*/
public FloatNativeBuffer(InterleavedComplexSamples samples)
{
this(samples.samples(), samples.timestamp());
this(samples.samples(), samples.timestamp(), 0.0f);
}

@Override
Expand All @@ -90,12 +79,6 @@ public int sampleCount()
return mInterleavedComplexSamples.length / 2;
}

@Override
public long getTimestamp()
{
return mTimestamp;
}

private class ComplexSamplesIterator implements Iterator<ComplexSamples>
{
private boolean mEmpty;
Expand All @@ -115,7 +98,7 @@ public ComplexSamples next()
}

mEmpty = true;
return SampleUtils.deinterleave(mInterleavedComplexSamples);
return SampleUtils.deinterleave(mInterleavedComplexSamples, getTimestamp());
}
}

Expand All @@ -139,7 +122,7 @@ public InterleavedComplexSamples next()
}

mEmpty = true;
return new InterleavedComplexSamples(mInterleavedComplexSamples, mTimestamp);
return new InterleavedComplexSamples(mInterleavedComplexSamples, getTimestamp());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ public interface INativeBufferFactory
*
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
* @return instance
*/
INativeBuffer getBuffer(ByteBuffer samples, long timestamp);

/**
* Sets the samples per millisecond rate based on the current sample rate.
*
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
*/
void setSamplesPerMillisecond(float samplesPerMillisecond);
}
24 changes: 11 additions & 13 deletions src/main/java/io/github/dsheirer/buffer/SignedByteNativeBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@

import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;

import java.util.Iterator;

/**
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
* non-interleaved complex sample buffers converted from the raw byte sample array.
*/
public class SignedByteNativeBuffer implements INativeBuffer
public class SignedByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private final static float[] LOOKUP_VALUES;
Expand All @@ -47,7 +46,6 @@ public class SignedByteNativeBuffer implements INativeBuffer
}

private byte[] mSamples;
private long mTimestamp;
private float mIAverageDc;
private float mQAverageDc;

Expand All @@ -57,27 +55,23 @@ public class SignedByteNativeBuffer implements INativeBuffer
* @param timestamp of the samples
* @param iAverageDc of the sample stream
* @param qAverageDc of the sample stream
* @param samplesPerMillisecond to calculate sub-buffer timestamps
*/
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc)
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);

//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % FRAGMENT_SIZE != 0)
{
throw new IllegalArgumentException("Samples byte[] length [" + samples.length + "] must be an even multiple of " + FRAGMENT_SIZE);
}

mSamples = samples;
mTimestamp = timestamp;
mIAverageDc = iAverageDc;
mQAverageDc = qAverageDc;
}

@Override
public long getTimestamp()
{
return mTimestamp;
}

@Override
public int sampleCount()
{
Expand Down Expand Up @@ -112,6 +106,8 @@ public boolean hasNext()
@Override
public ComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);

float[] i = new float[FRAGMENT_SIZE];
float[] q = new float[FRAGMENT_SIZE];
int samplesOffset = mSamplesPointer;
Expand All @@ -123,7 +119,7 @@ public ComplexSamples next()
}

mSamplesPointer = samplesOffset;
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

Expand All @@ -143,6 +139,8 @@ public boolean hasNext()
@Override
public InterleavedComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);

float[] converted = new float[FRAGMENT_SIZE * 2];

int samplesOffset = mSamplesPointer;
Expand All @@ -155,7 +153,7 @@ public InterleavedComplexSamples next()

mSamplesPointer = samplesOffset;

return new InterleavedComplexSamples(converted, mTimestamp);
return new InterleavedComplexSamples(converted, timestamp);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/**
* Implements a factory for creating SignedByteNativeBuffer instances
*/
public class SignedByteNativeBufferFactory implements INativeBufferFactory
public class SignedByteNativeBufferFactory extends AbstractNativeBufferFactory
{
/**
* DC removal calculations will run once a minute
Expand Down Expand Up @@ -78,7 +78,7 @@ public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
calculateDc(copy);
}

return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc);
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc, getSamplesPerMillisecond());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;

import java.util.Iterator;

/**
Expand All @@ -42,8 +41,9 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
protected float[] mQBuffer = new float[FRAGMENT_SIZE + Q_OVERLAP];
protected short[] mSamples;
protected int mSamplesPointer = 0;
protected long mTimestamp;
protected float mAverageDc;
private long mTimestamp;
private float mSamplesPerMillisecond;

/**
* Constructs an instance
Expand All @@ -52,8 +52,10 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
* @param residualQ samples from previous buffer
* @param averageDc as measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
float samplesPerMillisecond)
{
if(residualI.length != I_OVERLAP || residualQ.length != Q_OVERLAP)
{
Expand All @@ -73,6 +75,7 @@ public AirspyBufferIterator(short[] samples, short[] residualI, short[] residual
mAverageDc = averageDc;
mSamples = samples;
mTimestamp = timestamp;
mSamplesPerMillisecond = samplesPerMillisecond;

//Transfer and scale the residual I & Q samples from the previous buffer
for(int i = 0; i < residualI.length; i++)
Expand All @@ -86,6 +89,17 @@ public AirspyBufferIterator(short[] samples, short[] residualI, short[] residual
}
}

/**
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
* native buffer starting timestamp.
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
* @return timestamp adjusted to the fragment or sub-buffer start sample.
*/
protected long getFragmentTimestamp(int samplesPointer)
{
return mTimestamp + (long)(samplesPointer / 2 / mSamplesPerMillisecond);
}

@Override
public boolean hasNext()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.sample.complex.ComplexSamples;

import java.util.Arrays;

/**
Expand All @@ -36,10 +35,11 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -50,6 +50,8 @@ public ComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);

int offset = mSamplesPointer;

for(int x = 0; x < FRAGMENT_SIZE; x++)
Expand Down Expand Up @@ -91,6 +93,6 @@ public ComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
Expand All @@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -55,6 +56,7 @@ public ComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -121,6 +123,6 @@ public ComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
Expand All @@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -55,6 +56,7 @@ public ComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -115,6 +117,6 @@ public ComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
Expand All @@ -42,10 +41,12 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
mFilterPart2 = new float[16];
System.arraycopy(COEFFICIENTS, 16, mFilterPart2, 8, 8);
Expand All @@ -59,6 +60,8 @@ public ComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);

int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -117,6 +120,6 @@ public ComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
Expand All @@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -55,6 +56,8 @@ public ComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);

int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -133,6 +136,6 @@ public ComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -49,6 +50,7 @@ public InterleavedComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;

for(int x = 0; x < FRAGMENT_SIZE; x++)
Expand Down Expand Up @@ -89,6 +91,6 @@ public InterleavedComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -54,6 +55,7 @@ public InterleavedComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -118,6 +120,6 @@ public InterleavedComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -54,6 +55,7 @@ public InterleavedComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -112,6 +114,6 @@ public InterleavedComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
mFilterPart2 = new float[16];
Expand All @@ -59,6 +60,7 @@ public InterleavedComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -115,6 +117,6 @@ public InterleavedComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

@Override
Expand All @@ -54,6 +55,7 @@ public InterleavedComplexSamples next()
throw new IllegalStateException("End of buffer exceeded");
}

long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
Expand Down Expand Up @@ -130,6 +132,6 @@ public InterleavedComplexSamples next()
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);

return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,21 @@

package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.vector.calibrate.Implementation;

import java.util.Iterator;

/**
* Native buffer scalar implementation for Airspy non-packed samples.
*/
public class AirspyNativeBuffer implements INativeBuffer
public class AirspyNativeBuffer extends AbstractNativeBuffer
{
private short[] mSamples;
private short[] mResidualI;
private short[] mResidualQ;
private float mAverageDc;
private long mTimestamp;
private Implementation mInterleavedImplementation;
private Implementation mNonInterleavedImplementation;

Expand All @@ -48,11 +46,14 @@ public class AirspyNativeBuffer implements INativeBuffer
* @param timestamp of the buffer
* @param interleavedImplementation optimal, scalar vs vector SIMD
* @param nonInterleavedImplementation optimal, scalar vs vector SIMD
* @param samplesPerMillisecond used to calculate sub-buffer fragment timestamp offsets from the start of this buffer.
*/
public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, Implementation interleavedImplementation,
Implementation nonInterleavedImplementation)
Implementation nonInterleavedImplementation, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);

//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % AirspyBufferIterator.FRAGMENT_SIZE != 0)
{
Expand All @@ -64,7 +65,6 @@ public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ,
mResidualI = residualI;
mResidualQ = residualQ;
mAverageDc = averageDc;
mTimestamp = timestamp;
mInterleavedImplementation = interleavedImplementation;
mNonInterleavedImplementation = nonInterleavedImplementation;
}
Expand All @@ -75,22 +75,16 @@ public int sampleCount()
return mSamples.length / 2;
}

@Override
public long getTimestamp()
{
return mTimestamp;
}

@Override
public Iterator<ComplexSamples> iterator()
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}

Expand All @@ -99,11 +93,11 @@ public Iterator<InterleavedComplexSamples> iteratorInterleaved()
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,18 @@

package io.github.dsheirer.buffer.airspy;

import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.INativeBufferFactory;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;

import java.nio.ByteBuffer;
import java.util.Arrays;

/**
* Implements a factory for creating SignedByteNativeBuffer instances
*/
public class AirspyNativeBufferFactory implements INativeBufferFactory
public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
{
private boolean mSamplePacking = false;
private short[] mResidualI = new short[AirspyBufferIterator.I_OVERLAP];
Expand Down Expand Up @@ -106,7 +105,7 @@ public INativeBuffer getBuffer(ByteBuffer buffer, long timestamp)
INativeBuffer nativeBuffer = new AirspyNativeBuffer(samples,
Arrays.copyOf(mResidualI, mResidualI.length),
Arrays.copyOf(mResidualQ, mResidualQ.length), mConverter.getAverageDc(), timestamp,
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation);
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation, getSamplesPerMillisecond());

extractResidual(samples);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractComplexPolyphaseChannelizer implements Listener<InterleavedComplexSamples>, ISourceEventListener
{
Expand All @@ -38,6 +37,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
private int mChannelCount;
private int mSubChannelCount;
private double mChannelSampleRate;
protected long mCurrentSamplesTimestamp;

/**
* Complex sample polyphase channelizer
Expand Down Expand Up @@ -106,7 +106,7 @@ protected void dispatch(List<float[]> channelResultsList)
{
for(PolyphaseChannelSource channel : mChannels)
{
channel.receiveChannelResults(channelResultsList);
channel.receiveChannelResults(channelResultsList, mCurrentSamplesTimestamp);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.util.Dispatcher;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
Expand Down Expand Up @@ -193,7 +192,8 @@ public void setRates(double sampleRate, int channelCount)
@Override
public void receive(InterleavedComplexSamples complexSamples)
{
//TODO: how do we pass the timestamp onto the sample assemblers?
mCurrentSamplesTimestamp = complexSamples.timestamp();

float[] samples = complexSamples.samples();

int samplesPointer = 0;
Expand Down
Loading