#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

View File

@@ -1,10 +1,12 @@
<component name="ProjectDictionaryState">
<dictionary name="denny">
<words>
<w>bahroo</w>
<w>esnid</w>
<w>fleetsync</w>
<w>ident</w>
<w>polyphase</w>
<w>shoutcast</w>
<w>talkgroup</w>
<w>tgid</w>
</words>

View File

@@ -21,9 +21,10 @@
<root url="jar://$PROJECT_DIR$/imports/usb4java-javax-1.2.0.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jsr80-1.0.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-linux-arm.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.2.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-osx-x86_64.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/JTransforms-3.0.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/java-lame-3.98.4.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />

View File

@@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="jmbe-interface-0.3.3">
<CLASSES>
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="jump3r-1.0.3">
<CLASSES>
<root url="jar://$PROJECT_DIR$/imports/jump3r-1.0.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

Binary file not shown.

Binary file not shown.

View File

@@ -20,7 +20,5 @@
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" name="jmbe-interface-0.3.3" level="project" />
<orderEntry type="library" name="jump3r-1.0.3" level="project" />
</component>
</module>

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

View File

@@ -43,6 +43,7 @@ import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import audio.broadcast.AudioBroadcastManager;
import map.IconManager;
import map.MapService;
import module.log.EventLogManager;
@@ -152,6 +153,9 @@ public class SDRTrunk implements Listener<TunerEvent>
AudioManager audioManager = new AudioManager( threadPoolManager, sourceManager.getMixerManager() );
channelProcessingManager.addAudioPacketListener( audioManager );
AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager);
channelProcessingManager.addAudioPacketListener(audioBroadcastManager);
MapService mapService = new MapService( mSettingsManager );
channelProcessingManager.addMessageListener( mapService );

View File

@@ -0,0 +1,18 @@
package org.jma.encoder.audio;
/**
* Retrieved from: http://livertmpjavapublisher.blogspot.com/2011/06/orgtritonuslowlevellameapiiaudioencoder.html
* on 5 Sep 2016
*/
public interface IAudioEncoder
{
public int encodeBuffer(byte[] input, int offset, int encodedCount, byte[] output);
public int encodeFinish(byte[] input);
public int getInputBufferSize();
public int getOutputBufferSize();
public void close();
}

View File

@@ -0,0 +1,319 @@
/*******************************************************************************
* 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 record.mp3;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.convert.MP3AudioConverter;
import module.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.wave.AudioPacketMonoWaveReader;
import sample.Listener;
import util.TimeStamp;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* MP3 recorder for converting 8 kHz PCM audio packets to MP3 and writing to .mp3 file.
*/
public class MP3Recorder extends Module implements Listener<AudioPacket>, IAudioPacketListener
{
private final static Logger mLog = LoggerFactory.getLogger(MP3Recorder.class);
private MP3AudioConverter mMP3Converter;
private FileOutputStream mFileOutputStream;
private String mFilePrefix;
private Path mPath;
private BufferProcessor mBufferProcessor;
private ScheduledFuture<?> mProcessorHandle;
private LinkedBlockingQueue<AudioPacket> mAudioPacketQueue = new LinkedBlockingQueue<>(500);
private long mLastBufferReceived;
private AtomicBoolean mRunning = new AtomicBoolean();
/**
* MP3 audio recorder module for converting audio packets to 16 kHz constant bit rate MP3 format and
* recording to a file.
*/
public MP3Recorder(String filePrefix)
{
mFilePrefix = filePrefix;
int mp3BitRate = 16; //16 kHz
boolean variableBitRate = false; //CBR
mMP3Converter = new MP3AudioConverter(mp3BitRate, variableBitRate);
}
/**
* Timestamp of when the latest buffer was received by this recorder
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
}
public Path getPath()
{
return mPath;
}
/**
* Starts this recorder using as a scheduled thread broadcast running under the executor argument
*
* @param executor to use in scheduling MP3 conversion and file writes.
*/
public void start(ScheduledExecutorService executor)
{
if (mRunning.compareAndSet(false, true))
{
if (mBufferProcessor == null)
{
mBufferProcessor = new BufferProcessor();
}
try
{
StringBuilder sb = new StringBuilder();
sb.append(mFilePrefix);
sb.append("_");
sb.append(TimeStamp.getLongTimeStamp("_"));
sb.append(".mp3");
mPath = Paths.get(sb.toString());
mFileOutputStream = new FileOutputStream(mPath.toFile());
/* Schedule the handler to run every second */
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 200, TimeUnit.MILLISECONDS);
}
catch (IOException io)
{
mLog.error("Error starting real buffer recorder", io);
}
}
}
/**
* Stops the recorder.
*/
public void stop()
{
mRunning.set(false);
if (mProcessorHandle != null)
{
mProcessorHandle.cancel(true);
}
mProcessorHandle = null;
processAudioPacketQueue();
close();
}
/**
* Closes the recording file.
*/
private void close()
{
if (mFileOutputStream != null)
{
try
{
mFileOutputStream.close();
}
catch (IOException e)
{
mLog.error("Error closing output stream", e);
}
}
}
/**
* Primary data insert method designed to accept a stream of audio packets and a final ending audio packet.
*
* Audio packets received before the recorder is started or after the recorder is stopped will be ignored.
*/
@Override
public void receive(AudioPacket audioPacket)
{
if (mRunning.get())
{
boolean success = mAudioPacketQueue.offer(audioPacket);
if (!success)
{
mLog.error("recorder buffer overflow - purging [" + mPath.toFile().getAbsolutePath() + "]");
mAudioPacketQueue.clear();
}
mLastBufferReceived = System.currentTimeMillis();
}
}
/**
* IAudioPacketListener interface method.
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
/**
* Disposes this audio recorder and prepares it for reclamation
*/
@Override
public void dispose()
{
stop();
}
/**
* Not implemented. Recorder modules are not appropriate for reset and reuse.
*/
@Override
public void reset()
{
}
/**
* Processes the audio packet queue.
*/
private void processAudioPacketQueue()
{
try
{
List<AudioPacket> mPacketsToProcess = new ArrayList<>();
mAudioPacketQueue.drainTo(mPacketsToProcess);
if (!mPacketsToProcess.isEmpty())
{
boolean stopRequested = false;
int stopPacketIndex = 0;
//An END audio packet is a request to stop the recorder
for (AudioPacket packet : mPacketsToProcess)
{
if (packet.getType() == AudioPacket.Type.END)
{
stopRequested = true;
stopPacketIndex = mPacketsToProcess.indexOf(packet);
continue;
}
}
if (stopRequested)
{
mRunning.set(false);
mAudioPacketQueue.clear();
if (stopPacketIndex > 0)
{
List<AudioPacket> remainingPacketsToProcess = new ArrayList<>();
for (int x = 0; x < stopPacketIndex; x++)
{
remainingPacketsToProcess.add(mPacketsToProcess.get(x));
}
write(remainingPacketsToProcess);
remainingPacketsToProcess.clear();
}
stop();
}
else
{
write(mPacketsToProcess);
}
mPacketsToProcess.clear();
}
}
catch (IOException ioe)
{
mAudioPacketQueue.clear();
mLog.error("IOException while processing audio packet queue for MP3 conversion and storage", ioe);
}
}
/**
* Converts the PCM audio packets to MP3 format and writes the converted audio to the output stream.
*/
private void write(List<AudioPacket> audioPackets) throws IOException
{
byte[] mp3Audio = mMP3Converter.convert(audioPackets);
mFileOutputStream.write(mp3Audio);
}
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
public void run()
{
if (mProcessing.compareAndSet(false, true))
{
processAudioPacketQueue();
mProcessing.set(false);
}
}
}
public static void main(String[] args)
{
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
final MP3Recorder recorder = new MP3Recorder("/home/denny/Music/denny_test");
recorder.start(Executors.newSingleThreadScheduledExecutor());
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(recorder);
reader.read();
recorder.stop();
}
catch (IOException e)
{
mLog.error("Error", e);
}
mLog.debug("Finished");
}
}

View File

@@ -0,0 +1,70 @@
/*******************************************************************************
* 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 record.wave;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.metadata.AudioMetadata;
import sample.Listener;
import sample.real.RealBuffer;
import java.io.IOException;
import java.nio.file.Path;
public class AudioPacketMonoWaveReader extends MonoWaveReader implements Listener<RealBuffer>
{
private AudioMetadata mMetadata = new AudioMetadata(0, false);
private IAudioPacketListener mListener;
/**
* Reads wav files and converts the contents to AudioPackets.
*
* @param path of file to read containing 8kHz, 16-bit signed litte endian PCM audio
* @param realTime causes the read() method to read and dispatch audio packets in real time. Note: when using
* real time playback, the thread calling the read() method will sleep as needed to effect real-time playback,
* therefore you should invoke read() on a separate thread if this will impact the calling thread.
*
* @throws IOException on any file IO errors
*/
public AudioPacketMonoWaveReader(Path path, boolean realTime) throws IOException
{
super(path, realTime);
setListener(this);
}
public void setMetadata(AudioMetadata metadata)
{
mMetadata = metadata;
}
public void setListener(IAudioPacketListener listener)
{
mListener = listener;
}
@Override
public void receive(RealBuffer realBuffer)
{
if(mListener != null)
{
mListener.getAudioPacketListener().receive(new AudioPacket(realBuffer.getSamples(), mMetadata));
}
}
}

View File

@@ -0,0 +1,257 @@
/*******************************************************************************
* 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 record.wave;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.ConversionUtils;
import sample.Listener;
import sample.real.RealBuffer;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.regex.Pattern;
public class MonoWaveReader implements AutoCloseable
{
private final static Logger mLog = LoggerFactory.getLogger( MonoWaveReader.class);
private static final Pattern FILENAME_PATTERN = Pattern.compile( "(.*_)(\\d+)(\\.wav)" );
private Path mPath;
private boolean mRealTime;
private InputStream mInputStream;
private int mDataByteSize = 0;
private Listener<RealBuffer> mListener;
private byte[] mDataBuffer = new byte[8000]; //500 milliseconds of audio
/**
* Wave file reader for PCM 8kHz, 16-bit little-endian format.
*
* @param path of the wave file
* @param realTime to force replay to real-time. Note: you should run this reader on a separate thread if you
* choose real time since this reader will sleep the calling thread as needed to effect real-time playback.
*/
public MonoWaveReader(Path path, boolean realTime) throws IOException
{
assert( path != null );
mPath = path;
mRealTime = realTime;
open();
}
/**
* Opens the file
*/
private void open() throws IOException
{
if(!Files.exists(mPath))
{
throw new IOException("File not found");
}
mInputStream = Files.newInputStream(mPath, StandardOpenOption.READ);
//Check for RIFF header
byte[] buffer = new byte[4];
mInputStream.read(buffer);
if(!Arrays.equals(buffer, WaveUtils.RIFF_CHUNK))
{
throw new IOException("File is not .wav format - missing RIFF chunk");
}
//Get file size
mInputStream.read(buffer);
int fileSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
//Check for WAVE format
mInputStream.read(buffer);
if(!Arrays.equals(buffer, WaveUtils.WAV_FORMAT))
{
throw new IOException("File is not .wav format - missing WAVE format");
}
//Check for format chunk
mInputStream.read(buffer);
if(!Arrays.equals(buffer, WaveUtils.CHUNK_FORMAT))
{
throw new IOException("File is not .wav format - missing format chunk");
}
//Get chunk size
mInputStream.read(buffer);
int chunkSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
//Get format
mInputStream.read(buffer);
ShortBuffer shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short format = shortBuffer.get();
if(format != WaveUtils.PCM_FORMAT)
{
throw new IOException("File format not supported - expecting PCM format");
}
//Get number of channels
short channels = shortBuffer.get();
if(channels != 1)
{
throw new IOException("Unsupported channel count - mono audio only");
}
//Get samples per second
mInputStream.read(buffer);
int sampleRate = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
//Get bytes per second
mInputStream.read(buffer);
int bytesPerSecond = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
mInputStream.read(buffer);
//Get frame size
shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short frameSize = shortBuffer.get();
if(frameSize != 2)
{
throw new IOException("PCM frame size not supported - expecting 2 bytes per frame");
}
//Get bits per sample
short bitsPerSample = shortBuffer.get();
if(bitsPerSample != 16)
{
throw new IOException("PCM sample size not supported - expecting 16 bits per sample");
}
mInputStream.read(buffer);
if(!Arrays.equals(buffer, WaveUtils.CHUNK_DATA))
{
throw new IOException("Unexpected chunk - expecting data chunk");
}
//Get data chunk size
mInputStream.read(buffer);
mDataByteSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
}
public void read() throws IOException
{
long start = System.currentTimeMillis();
long samplesRead = 0;
int totalBytesRead = 0;
while(totalBytesRead < mDataByteSize)
{
int bytesRead = mInputStream.read(mDataBuffer);
totalBytesRead += bytesRead;
float[] samples = ConversionUtils.convertFromSigned16BitSamples(mDataBuffer);
if(bytesRead != mDataBuffer.length)
{
samples = Arrays.copyOf(samples, bytesRead / 2);
}
if(mRealTime)
{
samplesRead += samples.length;
long actualElapsed = System.currentTimeMillis() - start;
long audioElapsed = (long)(((double)samplesRead / 8000.0) * 1000.0);
if(audioElapsed > actualElapsed)
{
try
{
Thread.sleep(audioElapsed - actualElapsed);
}
catch (InterruptedException e)
{
}
}
}
if(mListener != null)
{
mListener.receive(new RealBuffer(samples));
}
}
close();
}
public void setListener(Listener<RealBuffer> listener)
{
mListener = listener;
}
/**
* Closes the file
*/
public void close() throws IOException
{
if(mInputStream != null)
{
mInputStream.close();
}
}
public static void main(String[] args)
{
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
try
{
MonoWaveReader reader = new MonoWaveReader(path, true);
reader.setListener(new Listener<RealBuffer>()
{
@Override
public void receive(RealBuffer realBuffer)
{
mLog.debug("Received buffer");
}
});
reader.read();
}
catch(IOException e)
{
mLog.error("Error", e);
}
mLog.debug("Finished");
}
}

View File

@@ -17,14 +17,19 @@
******************************************************************************/
package record.wave;
import javax.sound.sampled.AudioFormat;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.sound.sampled.AudioFormat;
public class WaveUtils
{
public static final byte[] RIFF_CHUNK = {(byte)0x52, (byte)0x49, (byte)0x46, (byte)0x46}; //RIFF
public static final byte[] WAV_FORMAT = {(byte)0x57, (byte)0x41, (byte)0x56, (byte)0x45}; //WAVE
public static final byte[] CHUNK_FORMAT = {(byte)0x66, (byte)0x6D, (byte)0x74, (byte)0x20}; //fmt space
public static final byte[] CHUNK_DATA = {(byte)0x64, (byte)0x61, (byte)0x74, (byte)0x61}; //data
public static final int PCM_FORMAT = 1;
/**
* Creates a wave file header with the RIFF chunk descriptor and the WAVE
* format chunk 1 and chunk 2 header