Compare commits

...

9 Commits

Author SHA1 Message Date
Denny Sheirer
e4a85a4ed7 #1386 Updates version to 0.5.0 (final) (#1387)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-25 07:11:25 -05:00
Denny Sheirer
f61a415b43 #1383 Updates tuner editor to resolve sequencing issue where a recently enabled tuner was having its enabled state set to true and then enabling the tuner, which caused the tuner editor to mis-configure the tuner editor panel. (#1385)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-25 07:03:28 -05:00
Denny Sheirer
29ed3b4553 #1287 Disables event panel filtering until the feature is fully implemented with #1368. The current partial implementation is preventing valid events from displaying for protocols like DMR and likely others. (#1384)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-25 06:45:50 -05:00
Denny Sheirer
a87309e792 #1378 Resolves potential error where tuner shutdown (because of USB I/O error) hangs and causes application to run out of memory. This patch doesn't resolve the original issue with the USB I/O error, but it does ensure that the tuner shutdown which is happening on the USB processing thread, can complete in a timely fashion so that the application doesn't (potentially) crash from out of memory. (#1382)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-24 09:10:17 -05:00
Denny Sheirer
ddb5b5ef87 #1380 Improves Icecast streaming error handling and preserves streaming error messages for broadcast state to display in streaming status panel. Resolves issue with streaming status panel not showing icons and stream type. (#1381)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-24 07:48:04 -05:00
Denny Sheirer
7a58bf85b6 #1312 Resolves issue with radio reference editor attempting to access county info for a site that has county ID set to 0 (ie no county specified). (#1379)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-24 04:45:21 -05:00
Doug Hoffman
8707eb2c88 Correctly identify padded MP3 frames (#1375) 2022-12-23 06:12:44 -05:00
Denny Sheirer
08499ac73f #1237 Ensure MP3 input sample rate is supported by LAME encoder settings. (#1376)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-23 06:06:10 -05:00
Dennis Sheirer
70170ff70f Revert "Disable resampling if requested sample rate not supported"
This reverts commit 7de28519a1.
2022-12-18 08:29:35 -05:00
20 changed files with 522 additions and 197 deletions

View File

@@ -28,7 +28,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.5.0-beta7'
version = '0.5.0'
sourceCompatibility = '17'
sourceSets {

View File

@@ -25,19 +25,15 @@ 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>
{
@@ -255,7 +251,6 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
return state != BroadcastState.CONNECTED && !state.isErrorState();
}

View File

@@ -26,13 +26,6 @@ import io.github.dsheirer.properties.SystemProperties;
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;
@@ -48,6 +41,12 @@ import java.util.concurrent.TimeUnit;
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>
{
@@ -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;
@@ -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());
@@ -575,15 +574,8 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
{
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:
@@ -653,8 +645,8 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
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;

View File

@@ -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
@@ -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 BroadcastServerType
@@ -23,17 +23,17 @@ 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;

View File

@@ -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
@@ -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
@@ -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
@@ -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()

View File

@@ -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
@@ -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;
@@ -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;
@@ -65,6 +76,7 @@ public class BroadcastStatusPanel extends JPanel
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);
@@ -72,6 +84,37 @@ public class BroadcastStatusPanel extends JPanel
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.
*/

View File

@@ -20,13 +20,20 @@ package io.github.dsheirer.audio.broadcast.icecast;
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;
@@ -42,13 +49,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
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);
@@ -143,8 +143,9 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
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());
@@ -315,10 +316,49 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
* 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
{
@@ -374,4 +414,97 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
}
}
}
/**
* 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");
}
}
}

View File

@@ -21,13 +21,19 @@ package io.github.dsheirer.audio.broadcast.icecast;
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;
@@ -37,14 +43,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
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);
@@ -215,7 +213,13 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
}
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();
}
}

View File

@@ -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;
@@ -29,8 +30,8 @@ import javax.sound.sampled.AudioFormat;
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)"),
@@ -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
@@ -56,7 +68,7 @@ public enum InputAudioFormat
*/
public static InputAudioFormat getDefault()
{
return SR_22050;
return SR_16000;
}
/**

View File

@@ -57,19 +57,20 @@ public class MP3AudioConverter implements IAudioConverter
public MP3AudioConverter(InputAudioFormat inputAudioFormat, MP3Setting setting, boolean normalizeAudio)
{
mInputAudioFormat = inputAudioFormat;
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
if(inputAudioFormat.getSampleRate() != mEncoder.getEffectiveSampleRate())
{
mLog.warn("MP3 encoder setting '" + setting + "' doesn't support sample rate '" + inputAudioFormat + "', resampling disabled. Increase bitrate or decrease sample rate to fix.");
mInputAudioFormat = InputAudioFormat.SR_8000;
mEncoder = LameFactory.getLameEncoder(mInputAudioFormat, 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);
}

View File

@@ -100,7 +100,7 @@ public class MP3Header
*/
public static boolean isPadded(byte[] frame, int offset)
{
return (frame[offset + 2] & 0x01) == 1;
return (frame[offset + 2] & 0x02) == 0x02;
}
/**

View File

@@ -19,6 +19,8 @@
package io.github.dsheirer.audio.convert;
import java.util.Arrays;
import java.util.List;
import net.sourceforge.lame.mp3.Lame;
/**
@@ -79,6 +81,30 @@ public enum MP3Setting
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()
{

View File

@@ -34,6 +34,11 @@ import io.github.dsheirer.rrapi.type.Type;
import io.github.dsheirer.rrapi.type.Voice;
import io.github.dsheirer.service.radioreference.RadioReference;
import io.github.dsheirer.util.ThreadPool;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
@@ -55,12 +60,6 @@ import jiconfont.javafx.IconNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Radio Reference editor for trunked radio systems
*/
@@ -293,7 +292,13 @@ public class SystemEditor extends VBox
List<EnrichedSite> enrichedSites = new ArrayList<>();
for(Site site: sites)
{
CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
CountyInfo countyInfo = null;
if(site.getCountyId() > 0)
{
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
}
enrichedSites.add(new EnrichedSite(site, countyInfo));
}

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.mp3.MP3Preference;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
@@ -99,12 +100,47 @@ public class MP3PreferenceEditor extends HBox
mMP3SettingComboBox.getItems().addAll(MP3Setting.values());
mMP3SettingComboBox.getSelectionModel().select(mMP3Preference.getMP3Setting());
mMP3SettingComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mMP3Preference.setMP3Setting(newValue));
.addListener((observable, oldValue, newValue) -> {
mMP3Preference.setMP3Setting(newValue);
updateAudioSampleRateComboBox();
});
}
return mMP3SettingComboBox;
}
/**
* Updates the input sample rate combo box values and alerts the user whenever we auto-change an input sample rate
* due to a change in the MP3 setting value.
*/
private void updateAudioSampleRateComboBox()
{
InputAudioFormat currentSelection = getAudioSampleRateComboBox().getSelectionModel().getSelectedItem();
MP3Setting setting = getMP3SettingComboBox().getSelectionModel().getSelectedItem();
getAudioSampleRateComboBox().getItems().clear();
getAudioSampleRateComboBox().getItems().addAll(setting.getSupportedSampleRates());
if(currentSelection != null && getAudioSampleRateComboBox().getItems().contains(currentSelection))
{
getAudioSampleRateComboBox().getSelectionModel().select(currentSelection);
}
else
{
getAudioSampleRateComboBox().getSelectionModel().select(InputAudioFormat.getDefault());
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Sample Rate Updated");
alert.setHeaderText("Input Sample Rate Updated");
Label wrappingLabel = new Label("Previous input sample rate [" + currentSelection +
"] is not supported with encoder setting [" + setting + "]. Sample rate updated to default.");
wrappingLabel.setWrapText(true);
alert.getDialogPane().setContent(wrappingLabel);
alert.show();
}
}
private ComboBox<InputAudioFormat> getAudioSampleRateComboBox()
{
if(mAudioSampleRateComboBox == null)
@@ -113,7 +149,12 @@ public class MP3PreferenceEditor extends HBox
mAudioSampleRateComboBox.getItems().addAll(InputAudioFormat.values());
mAudioSampleRateComboBox.getSelectionModel().select(mMP3Preference.getAudioSampleRate());
mAudioSampleRateComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mMP3Preference.setAudioSampleRate(newValue));
.addListener((observable, oldValue, newValue) -> {
if(newValue != null)
{
mMP3Preference.setAudioSampleRate(newValue);
}
});
}
return mAudioSampleRateComboBox;

View File

@@ -1,18 +1,20 @@
/*
* *********************************************************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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 the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
* *********************************************************************************************************************
* 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.icon;
@@ -22,15 +24,6 @@ import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.ImageIcon;
import java.awt.Image;
import java.io.IOException;
import java.io.InputStream;
@@ -43,6 +36,15 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.ImageIcon;
public class IconModel
{
@@ -241,7 +243,6 @@ public class IconModel
if(path != null)
{
Icon icon = new Icon("", path);
return getScaledIcon(icon.getIcon(), height);
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* 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.module.decode.event;
@@ -30,13 +27,13 @@ import io.github.dsheirer.module.decode.event.filter.DecodeEventFilterSet;
import io.github.dsheirer.module.decode.event.filter.EventFilterProvider;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class DecodeEventModel extends AbstractTableModel implements Listener<IDecodeEvent>, EventFilterProvider<IDecodeEvent>
{
@@ -149,10 +146,12 @@ public class DecodeEventModel extends AbstractTableModel implements Listener<IDe
*/
public void receive(final IDecodeEvent event)
{
if (!mEventFilterSet.passes(event))
{
return;
}
//Disabled until #1368 decode event is fully implemented
// if (!mEventFilterSet.passes(event))
// {
// return;
// }
if(!mEvents.contains(event))
{
mEvents.add(0, event);

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* 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.module.decode.event;
@@ -44,18 +41,24 @@ import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
import io.github.dsheirer.sample.Listener;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain>
{
@@ -94,7 +97,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
updateCellRenderers();
add(mEventManagementPanel, "span,growx");
//Disabled until #1368 decode event is fully implemented
// add(mEventManagementPanel, "span,growx");
mEmptyScroller = new JScrollPane(mTable);
add(mEmptyScroller);

View File

@@ -116,6 +116,24 @@ public class MP3Preference extends Preference
{
mInputAudioFormat = InputAudioFormat.getDefault();
}
//Ensure the input sample rate is supported by the MP3 setting, or change it to the first supported rate.
MP3Setting setting = getMP3Setting();
if(!setting.getSupportedSampleRates().contains(mInputAudioFormat))
{
InputAudioFormat supportedAudioFormat = InputAudioFormat.getDefault();
//Default should always be supported by all mp3 settings, but fall-back to a supported rate if not.
if(!setting.getSupportedSampleRates().contains(supportedAudioFormat))
{
supportedAudioFormat = setting.getSupportedSampleRates().get(0);
}
mLog.warn("MP3 Setting [" + setting + "] does not support input sample rate [" + getAudioSampleRate().name() +
"] - updating to supported sample rate [" + supportedAudioFormat.name() + "]");
setAudioSampleRate(supportedAudioFormat);
}
}
return mInputAudioFormat;

View File

@@ -24,11 +24,10 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
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;
/**
* A discovered tuner that may be accessible for use.
@@ -106,8 +105,8 @@ public abstract class DiscoveredTuner implements ITunerErrorListener
if(mEnabled)
{
setTunerStatus(TunerStatus.ENABLED);
start();
setTunerStatus(TunerStatus.ENABLED);
}
else
{

View File

@@ -26,6 +26,7 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -267,9 +268,30 @@ public abstract class USBTunerController extends TunerController
public final void stop()
{
mRunning = false;
stopStreaming();
mNativeBufferBroadcaster.clear();
deviceStop();
//Spin the shutdown onto a new thread so that we can set a max wait threshold.
Thread t = new Thread(() -> {
stopStreaming();
mNativeBufferBroadcaster.clear();
deviceStop();
});
t.start();
try
{
//Wait up to 500 milliseconds for the shutdown to complete ... otherwise interrupt it and finish shutdown
t.join(500);
if(t.isAlive())
{
mLog.info("Tuner shutdown exceeded 500ms - forcing shutdown");
t.interrupt();
}
}
catch(InterruptedException ie)
{
}
if(mDeviceHandle != null)
{
@@ -495,6 +517,9 @@ public abstract class USBTunerController extends TunerController
private List<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mInProgressTransfers = new LinkedTransferQueue<>();
private boolean mAutoResubmitTransfers = false;
private int mTransferErrorCount = 0;
private List<Transfer> mErrorTransfers = new ArrayList<>();
private int mResubmitFailureLogCount = 0;
/**
* Creates USB Transfers to carry the streaming sample data. Transfer buffers are backed by native memory
@@ -559,34 +584,56 @@ public abstract class USBTunerController extends TunerController
int status = LibUsb.submitTransfer(transfer);
if(status == LibUsb.SUCCESS)
{
mInProgressTransfers.add(transfer);
}
else if(status == LibUsb.ERROR_BUSY)
{
mInProgressTransfers.add(transfer);
//Weird issue/bug with libusb on Windows where libusb tells us the transfer is complete and then tells
//us that the transfer is already submitted when we attempt to re-submit the transfer. We track the
//number of times this occurs and log when the anomaly count exceeds the number of transfer buffers
//in case the application gets to a state where all transfers are no longer usable ... so that we
//know post-mortem what happened to the application.
mAnomalousTransfersDetected++;
if(mAnomalousTransfersDetected < USB_BULK_TRANSFER_BUFFER_POOL_SIZE)
//Attempt to resubmit any previous transfers that failed on submit
if(!mErrorTransfers.isEmpty())
{
mLog.error("USB transfer anomaly detected - continuing - previous status [" + previousStatus +
" ] transferred [" + previousBytesTransferred + "] this has happened [" +
mAnomalousTransfersDetected + "] times");
Transfer toResubmit = mErrorTransfers.get(0);
int resubmitStatus = LibUsb.submitTransfer(toResubmit);
if(resubmitStatus == LibUsb.SUCCESS)
{
mInProgressTransfers.add(transfer);
mTransferErrorCount--;
}
else
{
mErrorTransfers.add(transfer);
if(mResubmitFailureLogCount < mAvailableTransfers.size())
{
mResubmitFailureLogCount++;
mLog.error("Attempt to resubmit previously failed transfer unsuccessful - status: " +
LibUsb.errorName(resubmitStatus));
}
if(mErrorTransfers.size() >= mAvailableTransfers.size())
{
mLog.error("Maximum USB I/O transfer submit errors reached - shutting down USB tuner");
ThreadPool.CACHED.submit(() -> setErrorMessage("USB Error - Transfers"));
}
}
}
}
else if(status == LibUsb.ERROR_BUSY || status == LibUsb.ERROR_IO)
{
mErrorTransfers.add(transfer);
mTransferErrorCount++;
//This should only log once, when the anomaly count matches the transfer count
if(mAnomalousTransfersDetected == USB_BULK_TRANSFER_BUFFER_POOL_SIZE)
//For ERROR_BUSY error, there is a weird issue/bug with libusb on Windows where libusb tells us the
//transfer is complete and then tells us that the transfer is already submitted when we attempt to
//re-submit the transfer. We track the number of times this occurs and log when the anomaly count
//exceeds the number of transfer buffers in case the application gets to a state where all transfers
//are no longer usable ... so that we know post-mortem what happened to the application.
mLog.error("USB error while submitting transfer buffer [" + LibUsb.errorName(status) + "] - this happened [" +
mTransferErrorCount + "] times, but may be temporary. Will attempt to resubmit in future");
if(mErrorTransfers.size() >= mAvailableTransfers.size())
{
mLog.warn("USB transfer anomaly detection count exceeded the total number [8] of available " +
"tranfers - sdrtrunk may no longer be streaming data from this tuner and may appear to " +
"be locked up. If so, restart the application to resolve the issue and please send the " +
"application log to the developer");
mLog.error("Maximum USB I/O transfer submit errors reached - shutting down USB tuner");
ThreadPool.CACHED.submit(() -> setErrorMessage("USB Error - Transfers"));
}
}
else
@@ -610,6 +657,10 @@ public abstract class USBTunerController extends TunerController
{
LibUsb.cancelTransfer(transfer);
}
for(Transfer transfer: mErrorTransfers)
{
LibUsb.cancelTransfer(transfer);
}
}
@Override
@@ -641,14 +692,15 @@ public abstract class USBTunerController extends TunerController
transfer.buffer().rewind();
break;
default:
//Unexpected transfer error - need to reset the bulk transfer interface
//Unexpected transfer error - shutdown the tuner
transfer.buffer().rewind();
//Only set an error if we're not shutting down
if(mAutoResubmitTransfers)
{
setErrorMessage("LibUsb Transfer Error - stopping device - status [" + transfer.status() + "] - " +
LibUsb.errorName(transfer.status()));
//spin this off onto the thread pool, so it doesn't impact the usb processor thread.
ThreadPool.CACHED.submit(() -> setErrorMessage("LibUsb Transfer Error - stopping device - " +
"status [" + transfer.status() + "] - " + LibUsb.errorName(transfer.status())));
}
break;
}