mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#30 - WIP Streaming audio support. Adds Shoutcast V1 handler.
This commit is contained in:
2
.idea/dictionaries/denny.xml
generated
2
.idea/dictionaries/denny.xml
generated
@@ -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>
|
||||
|
||||
3
.idea/libraries/imports.xml
generated
3
.idea/libraries/imports.xml
generated
@@ -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 />
|
||||
|
||||
9
.idea/libraries/jmbe_interface_0_3_3.xml
generated
9
.idea/libraries/jmbe_interface_0_3_3.xml
generated
@@ -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>
|
||||
9
.idea/libraries/jump3r_1_0_3.xml
generated
9
.idea/libraries/jump3r_1_0_3.xml
generated
@@ -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>
|
||||
BIN
imports/java-lame-3.98.4.jar
Normal file
BIN
imports/java-lame-3.98.4.jar
Normal file
Binary file not shown.
Binary file not shown.
@@ -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
58
src/audio/AudioUtils.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
205
src/audio/broadcast/Broadcaster.java
Normal file
205
src/audio/broadcast/Broadcaster.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/audio/broadcast/BroadcasterFactory.java
Normal file
80
src/audio/broadcast/BroadcasterFactory.java
Normal 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;
|
||||
}
|
||||
}
|
||||
134
src/audio/broadcast/configuration/BroadcastConfiguration.java
Normal file
134
src/audio/broadcast/configuration/BroadcastConfiguration.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
93
src/audio/broadcast/configuration/BroadcastMetadata.java
Normal file
93
src/audio/broadcast/configuration/BroadcastMetadata.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
141
src/audio/broadcast/configuration/ShoutcastV1Configuration.java
Normal file
141
src/audio/broadcast/configuration/ShoutcastV1Configuration.java
Normal 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;
|
||||
}
|
||||
}
|
||||
179
src/audio/broadcast/configuration/ShoutcastV2Configuration.java
Normal file
179
src/audio/broadcast/configuration/ShoutcastV2Configuration.java
Normal 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;
|
||||
}
|
||||
}
|
||||
140
src/audio/broadcast/handler/BroadcastHandler.java
Normal file
140
src/audio/broadcast/handler/BroadcastHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
91
src/audio/broadcast/handler/BroadcastState.java
Normal file
91
src/audio/broadcast/handler/BroadcastState.java
Normal 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;
|
||||
}
|
||||
}
|
||||
379
src/audio/broadcast/handler/ShoutcastV1Handler.java
Normal file
379
src/audio/broadcast/handler/ShoutcastV1Handler.java
Normal 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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
77
src/audio/convert/MP3AudioConverter.java
Normal file
77
src/audio/convert/MP3AudioConverter.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
|
||||
|
||||
18
src/org/jma/encoder/audio/IAudioEncoder.java
Normal file
18
src/org/jma/encoder/audio/IAudioEncoder.java
Normal 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();
|
||||
}
|
||||
319
src/record/mp3/MP3Recorder.java
Normal file
319
src/record/mp3/MP3Recorder.java
Normal 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");
|
||||
}
|
||||
}
|
||||
70
src/record/wave/AudioPacketMonoWaveReader.java
Normal file
70
src/record/wave/AudioPacketMonoWaveReader.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
257
src/record/wave/MonoWaveReader.java
Normal file
257
src/record/wave/MonoWaveReader.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user