#30 - WIP Streaming audio support. Adds Shoutcast V1 handler.

This commit is contained in:
Denny
2016-09-12 04:28:46 -04:00
parent 9031fd78c9
commit 2537a71375
38 changed files with 2316 additions and 207 deletions

58
src/audio/AudioUtils.java Normal file
View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.wave.RealBufferWaveRecorder;
import sample.ConversionUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
public class AudioUtils
{
private final static Logger mLog = LoggerFactory.getLogger(AudioUtils.class);
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
public static byte[] convertTo16BitSamples(List<AudioPacket> audioPackets)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
for(AudioPacket packet: audioPackets)
{
//Converting from 32-bit floats to signed 16-bit samples
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
stream.write(buffer.array());
}
}
catch(IOException e)
{
mLog.error("Error writing converted PCM bytes to output stream");
}
return stream.toByteArray();
}
}

View File

@@ -16,18 +16,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream;
package audio.broadcast;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import controller.ThreadPoolManager;
import sample.Listener;
public class StreamManager implements IAudioPacketListener, Listener<AudioPacket>
public class AudioBroadcastManager implements Listener<AudioPacket>
{
private ThreadPoolManager mThreadPoolManager;
public StreamManager(ThreadPoolManager threadPoolManager)
public AudioBroadcastManager(ThreadPoolManager threadPoolManager)
{
mThreadPoolManager = threadPoolManager;
}
@@ -37,10 +36,4 @@ public class StreamManager implements IAudioPacketListener, Listener<AudioPacket
{
}
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
}

View File

@@ -16,24 +16,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream;
package audio.broadcast;
import audio.broadcast.configuration.BroadcastConfiguration;
import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.List;
public class StreamModel extends AbstractTableModel
public class AudioBroadcastModel extends AbstractTableModel
{
private static final int COLUMN_CHANNEL_NAME = 0;
private static final int COLUMN_CHANNEL_FORMAT = 1;
private static final int COLUMN_SERVER_NAME = 2;
private List<Stream> mStreams = new ArrayList<>();
private List<BroadcastConfiguration> mBroadcastConfigurations = new ArrayList<>();
@Override
public int getRowCount()
{
return mStreams.size();
return mBroadcastConfigurations.size();
}
@Override
@@ -45,20 +47,20 @@ public class StreamModel extends AbstractTableModel
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
if( rowIndex <= mStreams.size())
if( rowIndex <= mBroadcastConfigurations.size())
{
Stream stream = mStreams.get(rowIndex);
BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex);
if(stream != null)
if(configuration != null)
{
switch(columnIndex)
{
case COLUMN_CHANNEL_NAME:
return stream.getChannel().getName();
return configuration.getAlias();
case COLUMN_CHANNEL_FORMAT:
return stream.getChannel().getSourceFormat().name();
return configuration.getBroadcastFormat().name();
case COLUMN_SERVER_NAME:
return stream.getServer().getName();
return configuration.getHost();
default:
break;
}

View File

@@ -0,0 +1,205 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.broadcast.configuration.BroadcastConfiguration;
import audio.broadcast.handler.BroadcastHandler;
import audio.broadcast.handler.BroadcastState;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class Broadcaster implements IAudioPacketListener, Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger( Broadcaster.class );
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture mScheduledTask;
private AudioQueueProcessor mAudioQueueProcessor;
private LinkedTransferQueue<AudioPacket> mAudioQueue = new LinkedTransferQueue<>();
private BroadcastHandler mBroadcastHandler;
private boolean mEnabled = true;
/**
* Broadcaster for sending broadcast audio to a remote broadcast server.
*/
public Broadcaster(ThreadPoolManager threadPoolManager, BroadcastHandler broadcastHandler)
{
mThreadPoolManager = threadPoolManager;
mBroadcastHandler = broadcastHandler;
mAudioQueueProcessor = new AudioQueueProcessor();
//Add broadcast state listener to stop processing if we have an error
mBroadcastHandler.addListener(new BroadcastStateMonitor());
}
/**
* Broadcast configuration used by this broadcaster
*/
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastHandler.getBroadcastConfiguration();
}
/**
* Current state of the broadcaster's connection with the remote server
*/
public BroadcastState getBroadcastState()
{
return mBroadcastHandler.getBroadcastState();
}
/**
* Registers the listener to receive broadcast state changes
*/
public void addListener(Listener<BroadcastState> listener)
{
mBroadcastHandler.addListener(listener);
}
/**
* Removes the listener from receiving broadcast state changes
*/
public void removeListener(Listener<BroadcastState> listener)
{
mBroadcastHandler.removeListener(listener);
}
/**
* IAudioPacketListener interface method
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
/**
* Disconnects the broadcaster and halts all broadcasting.
*/
public void stop()
{
mEnabled = false;
stopProcessor();
mBroadcastHandler.disconnect();
}
/**
* Pauses the broadcaster.
* @param paused set to true to pause and false to continue
*/
public void setPaused(boolean paused)
{
if(mEnabled)
{
mBroadcastHandler.setPaused(paused);
}
}
/**
* Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcast
* handler for broadcast to the remote server.
*/
public void receive(AudioPacket packet)
{
if(mEnabled)
{
startProcessor();
mAudioQueue.add(packet);
}
}
/**
* Starts the audio queue processor. The audio packet queue is serviced every 100 milliseconds. Subsequent calls
* to this method once running are ignored.
*/
private void startProcessor()
{
if(mEnabled && mScheduledTask == null)
{
if(mThreadPoolManager != null)
{
mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
mAudioQueueProcessor, 100, TimeUnit.MILLISECONDS );
}
}
}
/**
* Stops the audio queue processor
*/
private void stopProcessor()
{
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
}
/**
* Monitors the broadcast handler for error state and halts audio broadcasting
*/
public class BroadcastStateMonitor implements Listener<BroadcastState>
{
@Override
public void receive(BroadcastState broadcastState)
{
if(broadcastState.isErrorState())
{
stop();
}
}
}
/**
* Processes audio packets from the audio packet queue and broadcasts the audio
*/
public class AudioQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
List<AudioPacket> audioPackets = new ArrayList<>();
mAudioQueue.drainTo(audioPackets);
if(!audioPackets.isEmpty())
{
mBroadcastHandler.broadcast(audioPackets);
}
mProcessing.set(false);
}
}
}
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast;
import audio.broadcast.configuration.BroadcastConfiguration;
import audio.broadcast.configuration.BroadcastFormat;
import audio.broadcast.configuration.ShoutcastV1Configuration;
import audio.broadcast.handler.ShoutcastV1Handler;
import audio.convert.IAudioConverter;
import audio.convert.MP3AudioConverter;
import controller.ThreadPoolManager;
public class BroadcasterFactory
{
public static final int MP3_MONO_16_KHZ_BITRATE = 16;
public static final boolean MP3_NO_VARIABLE_BITRATE = false;
/**
* Creates an audio streaming broadcaster for the configuration
* @param threadPoolManager for creating an audio broadcasting connection thread
* @param configuration describing the server and audio types
* @return configured broadcaster or null
*/
public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
BroadcastConfiguration configuration)
{
IAudioConverter converter = getAudioConverter(configuration.getBroadcastFormat());
if(converter != null)
{
switch(configuration.getBroadcastServerType())
{
case BROADCASTIFY:
break;
case ICECAST:
break;
case SHOUTCAST_V1:
return new Broadcaster(threadPoolManager,
new ShoutcastV1Handler((ShoutcastV1Configuration)configuration, converter));
case UNKNOWN:
break;
}
}
return null;
}
/**
* Creates an audio convert to convert from 8 kHz PCM audio to the specified format
* @param format to convert to
* @return audio convert or null
*/
public static IAudioConverter getAudioConverter(BroadcastFormat format)
{
switch(format)
{
case MP3:
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_NO_VARIABLE_BITRATE);
}
return null;
}
}

View File

@@ -0,0 +1,134 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.configuration;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
public abstract class BroadcastConfiguration
{
private BroadcastFormat mBroadcastFormat;
private String mAlias;
private String mHost;
private int mPort;
private String mPassword;
/**
* Broadcast audio streaming configuration. Describes the streaming server, broadcast user authentication and
* audio broadcast details to use when connecting to a streaming server and broadcasting audio.
*/
public BroadcastConfiguration(BroadcastFormat format)
{
mBroadcastFormat = format;
}
public abstract BroadcastServerType getBroadcastServerType();
/**
* Alias identifying this broadcast configuration.
*/
public String getAlias()
{
return mAlias;
}
/**
* Sets the alias name that identifies/describes this configuration
* @param alias
*/
public void setAlias(String alias)
{
mAlias = alias;
}
/**
* Streaming server host name.
*/
public String getHost()
{
return mHost;
}
/**
* Host name or IP address of the streaming server
* @param host
*/
public void setHost(String host)
{
mHost = host;
}
/**
* Streaming server port number;
*/
public int getPort()
{
return mPort;
}
/**
* Port number of the streaming server to where the broadcast will be directed.
* @param port
*/
public void setPort(int port)
{
mPort = port;
}
public SocketAddress getAddress()
{
return new InetSocketAddress(getHost(), getPort());
}
/**
* Password to authenticate with the streaming server.
*/
public String getPassword()
{
return mPassword;
}
/**
* Sets the password used to authenticate the broadcast broadcast user to the streaming server
* @param password
*/
public void setPassword(String password)
{
mPassword = password;
}
/**
* Audio broadcast content type (e.g. mp3)
*/
public BroadcastFormat getBroadcastFormat()
{
return mBroadcastFormat;
}
/**
* Audio broadcast content type (e.g. mp3)
* @param broadcastFormat
*/
public void setBroadcastFormat(BroadcastFormat broadcastFormat)
{
mBroadcastFormat = broadcastFormat;
}
}

View File

@@ -16,19 +16,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.source;
package audio.broadcast.configuration;
public abstract class ShoutSource implements IShoutSource
public class BroadcastConfigurationFactory
{
private ShoutSourceFormat mFormat;
public static BroadcastConfiguration getConfiguration(BroadcastServerType serverType, BroadcastFormat format)
{
switch(serverType)
{
case BROADCASTIFY:
break;
case ICECAST:
break;
case SHOUTCAST_V1:
return new ShoutcastV1Configuration(format);
case UNKNOWN:
break;
}
public ShoutSource(ShoutSourceFormat format)
{
mFormat = format;
}
@Override
public ShoutSourceFormat getFormat()
{
return mFormat;
return null;
}
}

View File

@@ -16,20 +16,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.server;
package audio.broadcast.configuration;
public abstract class ShoutServer implements IShoutServer
public enum BroadcastFormat
{
private String mName;
MP3("audio/mpeg");
public ShoutServer(String name)
private String mValue;
private BroadcastFormat(String value)
{
mName = name;
mValue = value;
}
@Override
public String getName()
public String getValue()
{
return mName;
return mValue;
}
}

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.configuration;
public enum BroadcastMetadata
{
AIM("icy-aim:"),
AUDIO_BIT_RATE("icy-br:"),
CHANNELS("icy-channels:"),
CONTENT_TYPE("Content-Type:"),
DESCRIPTION("icy-description:"),
GENRE("icy-genre:"),
ICE_AUDIO_INFO("ice-audio-info:"),
ICQ("icy-icq:"),
INTERNET_RELAY_CHAT("icy-irc:"),
METADATA_INTERVAL("icy-metaint:"),
METADATA_REQUEST("icy-metadata:"),
PRE_BUFFER("icy-prebuffer:"),
PUBLIC("icy-pub:"),
RESET("icy-reset:"),
SERVER("server:"),
STREAM_NAME("icy-name:"),
SONG_TITLE("icy-title:"),
URL("icy-url:"),
USER_AGENT("User-Agent:"),
UNKNOWN("Unknown");
private String mTag;
private BroadcastMetadata(String tag)
{
mTag = tag;
}
public static final String COMMAND_TERMINATOR = "\n";
public String toString()
{
return mTag;
}
/**
* Encodes the metadata tag and value with an end-of-line character, appropriate for sending over the network.
*/
public String encode(String value)
{
if(value != null && !value.isEmpty())
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(value).append(COMMAND_TERMINATOR);
return sb.toString();
}
return null;
}
public String encode(boolean value)
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(value ? "1" : "0").append(COMMAND_TERMINATOR);
return sb.toString();
}
public String encode(BroadcastFormat broadcastFormat)
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(broadcastFormat.getValue()).append(COMMAND_TERMINATOR);
return sb.toString();
}
public String encode(int value)
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(String.valueOf(value)).append(COMMAND_TERMINATOR);
return sb.toString();
}
}

View File

@@ -16,9 +16,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.source;
package audio.broadcast.configuration;
public interface IShoutSource
public enum BroadcastServerType
{
public ShoutSourceFormat getFormat();
BROADCASTIFY,
ICECAST,
SHOUTCAST_V1,
SHOUTCAST_V2,
UNKNOWN;
}

View File

@@ -0,0 +1,141 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.configuration;
public class ShoutcastV1Configuration extends BroadcastConfiguration
{
private String mStreamName;
private String mGenre;
private boolean mPublic;
private int mChannels;
private int mBitRate;
private String mURL;
public ShoutcastV1Configuration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.SHOUTCAST_V1;
}
/**
* Stream name
*/
public String getStreamName()
{
return mStreamName;
}
/**
* Sets the stream name
* @param name
*/
public void setStreamName(String name)
{
mStreamName = name;
}
/**
* Stream genre
*/
public String getGenre()
{
return mGenre;
}
/**
* Stream genre
* @param genre
*/
public void setGenre(String genre)
{
mGenre = genre;
}
/**
* Public visibility of the broadcast
*/
public boolean isPublic()
{
return mPublic;
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
*/
public void setPublic(boolean isPublic)
{
mPublic = isPublic;
}
/**
* Number of audio channels in the broadcast
*/
public int getChannels()
{
return mChannels;
}
/**
* Sets the number of audio channels in the broadcast
*/
public void setChannels(int channels)
{
mChannels = channels;
}
/**
* Bit rate in bits per second
*/
public int getBitRate()
{
return mBitRate;
}
/**
* Bit rate
* @param bitRate in samples per second
*/
public void setBitRate(int bitRate)
{
mBitRate = bitRate;
}
/**
* URL associated with the broadcast where users can find additional details.
*/
public String getURL()
{
return mURL;
}
/**
* URL associated with the broadcast where users can find additional details.
* @param url
*/
public void setURL(String url)
{
mURL = url;
}
}

View File

@@ -0,0 +1,179 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.configuration;
public class ShoutcastV2Configuration extends BroadcastConfiguration
{
private String mMountPoint;
private String mStreamName;
private String mSongTitle;
private String mGenre;
private boolean mPublic;
private int mChannels;
private int mBitRate;
private String mURL;
public ShoutcastV2Configuration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.SHOUTCAST_V1;
}
public String getMountPoint()
{
return mMountPoint;
}
/**
* Sets the optional mount point. Shoutcast v1.x servers don't require a mount point, but v2.x servers do require
* the mount point.
*
* Note: if the supplied mount point contains a leading slash (/) it will be removed.
*
* @param mountPoint
*/
public void setMountPoint(String mountPoint)
{
//Strip the leading slash from the mount point if the user supplies it
mMountPoint = mountPoint.replace("/", "");
}
/**
* Stream name
*/
public String getStreamName()
{
return mStreamName;
}
/**
* Sets the stream name
* @param name
*/
public void setStreamName(String name)
{
mStreamName = name;
}
/**
* Song title
*/
public String getSongTitle()
{
return mSongTitle;
}
/**
* Set the song title
*/
public void setSongTitle(String title)
{
mSongTitle = title;
}
/**
* Stream genre
*/
public String getGenre()
{
return mGenre;
}
/**
* Stream genre
* @param genre
*/
public void setGenre(String genre)
{
mGenre = genre;
}
/**
* Public visibility of the broadcast
*/
public boolean isPublic()
{
return mPublic;
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
*/
public void setPublic(boolean isPublic)
{
mPublic = isPublic;
}
/**
* Number of audio channels in the broadcast
*/
public int getChannels()
{
return mChannels;
}
/**
* Sets the number of audio channels in the broadcast
*/
public void setChannels(int channels)
{
mChannels = channels;
}
/**
* Bit rate in bits per second
*/
public int getBitRate()
{
return mBitRate;
}
/**
* Bit rate
* @param bitRate in samples per second
*/
public void setBitRate(int bitRate)
{
mBitRate = bitRate;
}
/**
* URL associated with the broadcast where users can find additional details.
*/
public String getURL()
{
return mURL;
}
/**
* URL associated with the broadcast where users can find additional details.
* @param url
*/
public void setURL(String url)
{
mURL = url;
}
}

View File

@@ -0,0 +1,140 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.handler;
import audio.AudioPacket;
import audio.broadcast.configuration.BroadcastConfiguration;
import audio.convert.IAudioConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class BroadcastHandler
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastHandler.class );
protected BroadcastConfiguration mBroadcastConfiguration;
protected IAudioConverter mAudioConverter;
private List<Listener<BroadcastState>> mBroadcastStateListeners = new CopyOnWriteArrayList<>();
private BroadcastState mBroadcastState = BroadcastState.READY;
/**
* Abstract audio broadcast handler. Protocol-specific implementations will manage a connection with the remote
* broadcast audio server, convert audio using the audio converter argument and dispatch the converted audio to
* the remote server.
*
* @param broadcastConfiguration for configuring the handler
* @param audioConverter to convert audio packets to the desired output format
*/
public BroadcastHandler(BroadcastConfiguration broadcastConfiguration, IAudioConverter audioConverter)
{
mBroadcastConfiguration = broadcastConfiguration;
mAudioConverter = audioConverter;
}
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
/**
* Audio converter
*/
protected IAudioConverter getAudioConverter()
{
return mAudioConverter;
}
/**
* Processes and broadcasts the audio packets
*/
public abstract void broadcast(List<AudioPacket> audioPackets);
/**
* Disconnects from the remote server and disables the broadcaster.
*/
public abstract void disconnect();
/**
* Pauses or unpauses the broadcast
*/
public void setPaused(boolean paused)
{
if(!getBroadcastState().isErrorState())
{
if(paused)
{
setBroadcastState(BroadcastState.PAUSED);
}
else
{
setBroadcastState(BroadcastState.READY);
}
}
}
/**
* Registers the listener to receive broadcast state changes
*/
public void addListener(Listener<BroadcastState> listener)
{
mBroadcastStateListeners.add(listener);
}
/**
* Removes the listener from receiving broadcast state changes
*/
public void removeListener(Listener<BroadcastState> listener)
{
mBroadcastStateListeners.remove(listener);
}
/**
* Sets the state of the broadcast connection
*/
protected void setBroadcastState(BroadcastState state)
{
if(mBroadcastState != state)
{
mLog.debug("Changing State to: " + state);
mBroadcastState = state;
for(Listener<BroadcastState> listener: mBroadcastStateListeners)
{
listener.receive(state);
}
}
}
/**
* Current state of the broadcast connection
*/
public BroadcastState getBroadcastState()
{
return mBroadcastState;
}
public boolean connected()
{
return getBroadcastState() == BroadcastState.CONNECTED;
}
}

View File

@@ -0,0 +1,91 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.handler;
public enum BroadcastState
{
/**
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
*/
BROADCAST_ERROR("Broadcast Error", false),
/**
* Connected to the broadcast server and capable of streaming
*/
CONNECTED("Connected", false),
/**
* Connection interrupted, attempting to reconnect.
*/
CONNECTING("Connecting", false),
/**
* Disconnected from the broadcast server
*/
DISCONNECTED("Disconnected", true),
/**
* Invalid password
*/
INVALID_PASSWORD("Invalid Password", true),
/**
* Server is not known or reachable
*/
NO_SERVER("No Server", false),
/**
* Connected but paused. No audio is streaming.
*/
PAUSED("Paused", false),
/**
* Initial state with no connection attempted.
*/
READY("Ready", false),
/**
* Server host name or port is invalid
*/
UNKNOWN_HOST("Unknown Host or Invalid Port", true),
/**
* Unspecified error
*/
ERROR("Error", true);
private String mLabel;
private boolean mErrorState;
private BroadcastState(String label, boolean connected)
{
mLabel = label;
mErrorState = connected;
}
public String toString()
{
return mLabel;
}
public boolean isErrorState()
{
return mErrorState;
}
}

View File

@@ -0,0 +1,379 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.handler;
import audio.AudioPacket;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcasterFactory;
import audio.broadcast.configuration.BroadcastFormat;
import audio.broadcast.configuration.BroadcastMetadata;
import audio.broadcast.configuration.ShoutcastV1Configuration;
import audio.convert.IAudioConverter;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class ShoutcastV1Handler extends BroadcastHandler
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1Handler.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private long mLastConnectionAttempt = 0;
private Socket mSocket;
private DataOutputStream mOutputStream;
private DataInputStream mInputStream;
private static final int mBufferSize = 2000;
/**
* Creates a Shoutcast Version 1 broadcast handler.
* @param configuration
* @param audioConverter
*/
public ShoutcastV1Handler(ShoutcastV1Configuration configuration, IAudioConverter audioConverter)
{
super(configuration, audioConverter);
}
/**
* Shoutcast V1 Configuration information
*/
private ShoutcastV1Configuration getShoutcastConfiguration()
{
return (ShoutcastV1Configuration)getBroadcastConfiguration();
}
/**
* Process audio packets using Shoutcast V1 Protocol
* @param audioPackets
*/
@Override
public void broadcast(List<AudioPacket> audioPackets)
{
//If we're connected, send the audio, otherwise discard it
if(connect())
{
byte[] convertedAudio = mAudioConverter.convert(audioPackets);
if(convertedAudio != null)
{
try
{
send(convertedAudio);
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
}
}
}
}
/**
* Indicates if the audio handler is currently connected to the remote server and capable of streaming audio.
*
* This method will attempt to establish a connection or reconnect to the streaming server.
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected())
{
createConnection();
}
return connected();
}
/**
* Disconnect from the remote server
*/
@Override
public void disconnect()
{
}
/**
* Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected
* following a successful connection, attempts to reestablish a connection on a set interval
*/
private void createConnection()
{
if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS)
{
createSocket();
if(mSocket.isConnected())
{
mLog.debug("Sending credentials");
StringBuilder sb = new StringBuilder();
//Password
sb.append(getShoutcastConfiguration().getPassword()).append(BroadcastMetadata.COMMAND_TERMINATOR);
//Metadata
sb.append(BroadcastMetadata.STREAM_NAME.encode(getShoutcastConfiguration().getStreamName()));
sb.append(BroadcastMetadata.URL.encode(getShoutcastConfiguration().getURL()));
sb.append(BroadcastMetadata.PUBLIC.encode(getShoutcastConfiguration().isPublic()));
sb.append(BroadcastMetadata.GENRE.encode(getShoutcastConfiguration().getGenre()));
sb.append(BroadcastMetadata.AUDIO_BIT_RATE.encode(getShoutcastConfiguration().getBitRate()));
//End of connection string
sb.append(BroadcastMetadata.COMMAND_TERMINATOR);
try
{
send(sb.toString());
if(isInvalidResponse())
{
mLog.error("Invalid response after sending metadata");
return;
}
else
{
setBroadcastState(BroadcastState.CONNECTED);
}
}
catch(IOException ioe)
{
mLog.debug("Error while connecting to server", ioe);
setBroadcastState(BroadcastState.ERROR);
}
mLog.debug("Connection established");
}
}
}
/**
* Sends the string data to the remote server
* @param data to send
* @throws IOException if there is an error communicating with the remote server
*/
private void send(String data) throws IOException
{
mLog.debug("Sending [" + data + "]");
if(data != null && !data.isEmpty() && mOutputStream != null)
{
mOutputStream.writeBytes(data);
}
}
/**
* Sends the byte data to the remote server
* @param data to send
* @throws IOException if there is an error communicating with the remote server
*/
private void send(byte[] data) throws IOException
{
mLog.debug("Sending bytes:" + data.length);
if(connected())
{
int sent = 0;
while(sent < data.length)
{
int available = data.length - sent;
int sending = available <mBufferSize ? available : mBufferSize;
mOutputStream.write(data, sent, sending);
sent += sending;
}
}
}
private String getResponse() throws IOException
{
if(mInputStream != null)
{
int bytesAvailable = mInputStream.available();
if(bytesAvailable > 0)
{
byte[] responseBuffer = new byte[bytesAvailable];
int bytesRead = 0;
while(bytesRead < bytesAvailable)
{
bytesRead += mInputStream.read(responseBuffer, bytesRead, bytesAvailable - bytesRead);
}
return new String(responseBuffer);
}
}
return null;
}
private boolean isInvalidResponse()
{
String response;
try
{
response = getResponse();
}
catch(IOException e)
{
mLog.error("Error while retrieving server response message", e);
setBroadcastState(BroadcastState.ERROR);
return false;
}
if(response != null && !response.isEmpty())
{
if(response.startsWith("OK"))
{
return false;
}
else if(response.startsWith("Invalid Password"))
{
setBroadcastState(BroadcastState.INVALID_PASSWORD);
return true;
}
mLog.debug("Unrecognized server response:" + response);
return true;
}
return false;
}
/**
* Creates a socket connection to the remote server and sets the state to CONNECTING.
*/
private void createSocket()
{
if(mSocket == null)
{
mLog.debug("Creating socket");
try
{
mSocket = new Socket(getShoutcastConfiguration().getHost(),
getShoutcastConfiguration().getPort());
mOutputStream = new DataOutputStream(mSocket.getOutputStream());
mInputStream = new DataInputStream(mSocket.getInputStream());
}
catch(UnknownHostException uhe)
{
setBroadcastState(BroadcastState.UNKNOWN_HOST);
mLog.error("Unknown host or port. Unable to create connection to streaming server host[" +
getShoutcastConfiguration().getHost() + "] and port[" +
getShoutcastConfiguration().getPort() + "] - will reattempt connection periodically");
return;
}
catch(IOException ioe)
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error connecting to streaming server host[" +
getShoutcastConfiguration().getHost() + "] and port[" +
getShoutcastConfiguration().getPort() + "]", ioe);
return;
}
}
if(mSocket.isConnected())
{
setBroadcastState(BroadcastState.CONNECTING);
}
else
{
try
{
SocketAddress address = getShoutcastConfiguration().getAddress();
mSocket.connect(address);
setBroadcastState(BroadcastState.CONNECTING);
}
catch(UnknownHostException uhe)
{
setBroadcastState(BroadcastState.UNKNOWN_HOST);
}
catch(IOException e)
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error connecting to streaming server host[" +
getShoutcastConfiguration().getHost() + "] and port[" +
getShoutcastConfiguration().getPort() + "]", e);
}
}
}
public static void main(String[] args)
{
ShoutcastV1Configuration config = new ShoutcastV1Configuration(BroadcastFormat.MP3);
config.setAlias("Test Configuration");
config.setHost("localhost");
config.setPort(8000);
config.setPassword("denny3");
config.setStreamName("Denny's Audio Broadcast Test");
config.setGenre("Scanner Audio");
config.setPublic(true);
config.setURL("http://localhost:8000");
config.setBitRate(16);
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
final Broadcaster broadcaster = BroadcasterFactory.getBroadcaster(threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
mLog.debug("Registering and starting audio playback");
while(true)
{
mLog.debug("Playback started [" + path.toString() + "]");
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.read();
}
catch (IOException e)
{
mLog.error("Error", e);
}
mLog.debug("Playback ended [" + path.toString() + "]");
}
}
}

View File

@@ -16,12 +16,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.source;
package audio.convert;
public class MP3ShoutSource extends ShoutSource
import audio.AudioPacket;
import java.util.List;
public interface IAudioConverter
{
public MP3ShoutSource()
{
super(ShoutSourceFormat.MP3);
}
public byte[] convert(List<AudioPacket> audioPackets);
}

View File

@@ -0,0 +1,77 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.convert;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioUtils;
import net.sourceforge.lame.lowlevel.LameEncoder;
import net.sourceforge.lame.mp3.Lame;
import net.sourceforge.lame.mp3.MPEGMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.util.List;
public class MP3AudioConverter implements IAudioConverter
{
private final static Logger mLog = LoggerFactory.getLogger( MP3AudioConverter.class );
private LameEncoder mEncoder;
private ByteArrayOutputStream mMP3Stream = new ByteArrayOutputStream();
private byte[] mMP3Buffer;
/**
* Converts PCM 8kHz 16-bit Little Endian audio packets to Mono, Low Quality MP3 compressed audio.
*
* @param bitRate for converted MP3 audio
* @param variableBitRate (VBR) true or false for constant bit rate (CBR)
*/
public MP3AudioConverter(int bitRate, boolean variableBitRate)
{
mEncoder = new LameEncoder(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
bitRate, MPEGMode.MONO, Lame.QUALITY_LOW, variableBitRate);
mMP3Buffer = new byte[mEncoder.getPCMBufferSize()];
}
@Override
public byte[] convert(List<AudioPacket> audioPackets)
{
mMP3Stream.reset();
byte[] pcmBytes = AudioUtils.convertTo16BitSamples(audioPackets);
int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length);
int mp3BufferSize = 0;
int pcmBytesPosition = 0;
while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
{
pcmBytesPosition += pcmBufferSize;
pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
}
return mMP3Stream.toByteArray();
}
}

View File

@@ -15,7 +15,7 @@
* 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 audio.inverted;
package audio.invert;
import dsp.filter.FilterFactory;
import dsp.filter.FloatFIRFilter;
@@ -23,7 +23,7 @@ import dsp.filter.Window.WindowType;
import dsp.mixer.Oscillator;
/**
* Based on the AudioType applied, produces normal, muted, or audio inverted
* Based on the AudioType applied, produces normal, muted, or audio invert
* audio output.
*/
public class AudioAdapter

View File

@@ -15,7 +15,7 @@
* 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 audio.inverted;
package audio.invert;
import audio.InversionFrequency;
import sample.real.RealSampleListener;
@@ -24,7 +24,7 @@ import dsp.filter.FloatFIRFilter;
import dsp.mixer.Oscillator;
/**
* Applies audio inversion (or un-inversion) to a stream of float audio samples
* Applies audio inversion (or un-inversion) to a broadcast of float audio samples
* by multiplying each successive sample by either a 1 or a -1 value.
*/
public class AudioInverter implements RealSampleListener

View File

@@ -15,7 +15,7 @@
* 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 audio.inverted;
package audio.invert;
public enum AudioType
{

View File

@@ -15,7 +15,7 @@
* 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 audio.inverted;
package audio.invert;
public interface IAudioTypeListener

View File

@@ -1,55 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream;
import audio.AudioPacket;
import audio.stream.shout.channel.IShoutChannel;
import audio.stream.shout.server.IShoutServer;
import sample.Listener;
/**
* Streaming channel encapsulates communication with a specific server and channel.
*/
public class Stream implements Listener<AudioPacket>
{
private IShoutServer mServer;
private IShoutChannel mChannel;
public Stream(IShoutServer server, IShoutChannel channel)
{
mServer = server;
mChannel = channel;
}
public IShoutServer getServer()
{
return mServer;
}
public IShoutChannel getChannel()
{
return mChannel;
}
@Override
public void receive(AudioPacket audioPacket)
{
}
}

View File

@@ -1,27 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.channel;
import audio.stream.shout.source.ShoutSourceFormat;
public interface IShoutChannel
{
public String getName();
public ShoutSourceFormat getSourceFormat();
}

View File

@@ -1,24 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.server;
public interface IShoutServer
{
public String getName();
}

View File

@@ -1,24 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.stream.shout.source;
public enum ShoutSourceFormat
{
MP3;
}