#30 WIP - broadcastify multi-channel streaming is functional.

This commit is contained in:
Denny
2016-11-27 11:58:34 -05:00
parent ecfac83eb4
commit b71415b82a
37 changed files with 2049 additions and 715 deletions

View File

@@ -9,6 +9,7 @@
<w>ident</w>
<w>polyphase</w>
<w>samplerate</w>
<w>sdrtrunk</w>
<w>shoutcast</w>
<w>streamable</w>
<w>talkgroup</w>

View File

@@ -212,6 +212,7 @@ public class AliasList implements Listener<AliasEvent>
mTalkgroup.put(tgid, alias);
}
break;
case BROADCAST_CHANNEL:
case NON_RECORDABLE:
case PRIORITY:
//We don't maintain lookups for these items

View File

@@ -23,16 +23,42 @@ import alias.id.AliasIDType;
import javax.xml.bind.annotation.XmlAttribute;
public class BroadcastChannel extends AliasID
public class BroadcastChannel extends AliasID implements Comparable<BroadcastChannel>
{
private String mChannelName;
public BroadcastChannel()
{
//JAXB Constructor
}
@Override
public int compareTo(BroadcastChannel other)
{
if(mChannelName != null && other.getChannelName() != null)
{
return mChannelName.compareTo(other.getChannelName());
}
else if(mChannelName != null)
{
return -1;
}
else
{
return 1;
}
}
/**
* Name of the broadcast channel configuration
* Creates a named broadcast channel
*/
public BroadcastChannel(String channelName)
{
mChannelName = channelName;
}
/**
* Name of the broadcastAudio channel configuration
*/
@XmlAttribute(name="channel")
public String getChannelName()
@@ -41,7 +67,7 @@ public class BroadcastChannel extends AliasID
}
/**
* Sets the name of the broadcast channel configuration
* Sets the name of the broadcastAudio channel configuration
*/
public void setChannelName(String channel)
{

View File

@@ -19,12 +19,15 @@
package alias.id.broadcast;
import alias.id.AliasID;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastModel;
import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
@@ -40,16 +43,14 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
+ "streaming server";
private JComboBox<String> mBroadcastConfigurations;
private BroadcastModel mBroadcastModel;
public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
{
List<String> channelNames = broadcastModel.getBroadcastConfigurationNames();
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
setItem( aliasID );
mBroadcastModel = broadcastModel;
initGUI();
setItem( aliasID );
}
private void initGUI()
@@ -58,15 +59,20 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
add( new JLabel( "Broadcast Channel" ), "grow" );
List<String> channelNames = mBroadcastModel.getBroadcastConfigurationNames();
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
mBroadcastConfigurations.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mBroadcastConfigurations, "grow");
BroadcastChannel channel = (BroadcastChannel)getItem();
if(channel.isValid())
{
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
}
JLabel help = new JLabel( "Help ..." );
help.setForeground( Color.BLUE.brighter() );
help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
@@ -82,10 +88,42 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
add( help, "align left" );
}
@Override
public void setItem(AliasID item)
{
super.setItem(item);
if(item instanceof BroadcastChannel)
{
BroadcastChannel channel = (BroadcastChannel)item;
if(channel.isValid())
{
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
}
}
setModified(false);
}
@Override
public void save()
{
((BroadcastChannel)getItem()).setChannelName((String)mBroadcastConfigurations.getSelectedItem());
String channel = (String)mBroadcastConfigurations.getSelectedItem();
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getBroadcastConfiguration(channel);
if(selectedConfiguration != null)
{
((BroadcastChannel)getItem()).setChannelName(selectedConfiguration.getName());
}
else
{
((BroadcastChannel)getItem()).setChannelName(null);
}
}
}

View File

@@ -1,39 +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.broadcast;
import audio.AudioPacket;
import controller.ThreadPoolManager;
import sample.Listener;
public class AudioBroadcastManager implements Listener<AudioPacket>
{
private ThreadPoolManager mThreadPoolManager;
public AudioBroadcastManager(ThreadPoolManager threadPoolManager)
{
mThreadPoolManager = threadPoolManager;
}
@Override
public void receive(AudioPacket audioPacket)
{
}
}

View File

@@ -0,0 +1,425 @@
/*******************************************************************************
* 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.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Listener;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AudioBroadcaster implements IAudioPacketListener, Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger( AudioBroadcaster.class );
public static final int PROCESSOR_RUN_INTERVAL_MS = 1000;
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture mScheduledTask;
private StreamManager mStreamManager;
private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
private ArrayBlockingQueue<StreamableAudioRecording> mAudioRecordingQueue;
private byte[] mSilenceFrame;
private int mStreamedAudioCount = 0;
private Listener<BroadcastEvent> mBroadcastEventListener;
private BroadcastState mBroadcastState = BroadcastState.READY;
private BroadcastConfiguration mBroadcastConfiguration;
private boolean mEnabled = true;
/**
* AudioBroadcaster for sending audio to a remote broadcastAudio server.
*/
public AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
{
mThreadPoolManager = threadPoolManager;
mBroadcastConfiguration = broadcastConfiguration;
//Recording queue - bounded at 10 queued recordings maximum - new stream recordings will be deleted once full
mAudioRecordingQueue = new ArrayBlockingQueue<>(10);
//Create a 1 second silence frame - from 1200 millis of silence
mSilenceFrame = BroadcastFactory.getSilenceFrame(getBroadcastConfiguration().getBroadcastFormat(), 1200);
mStreamManager = new StreamManager(threadPoolManager, this, broadcastConfiguration.getDelay(),
SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY));
mStreamManager.start();
startProcessor();
}
/**
* Recording backlog awaiting streaming
*/
public int getQueueSize()
{
return mAudioRecordingQueue.size();
}
/**
* Number of audio recordings streamed to remote server
*/
public int getStreamedAudioCount()
{
return mStreamedAudioCount;
}
/**
* Queues the streamable audio recording for broadcastAudio. The bounded queue is managed by removing older recordings
* at the head of the queue, once full, to make room for new recordings. This ensures that once the broadcaster
* (re)starts broadcasting or catches up with the queue, that it is broadcasting the most current audio recordings.
*
* @param recording to queue for broadcasting
*/
public void receive(StreamableAudioRecording recording)
{
boolean success = false;
try
{
success = mAudioRecordingQueue.add(recording);
}
catch(IllegalStateException ise)
{
//The queue is full - delete the oldest recording at the head of the queue to make room for a new one
removeRecording(mAudioRecordingQueue.poll());
}
if(!success)
{
try
{
success = mAudioRecordingQueue.add(recording);
}
catch(IllegalStateException ise)
{
//Do nothing on second attempt
}
}
//If we can't queue the recording after two attempts, remove the recording
if(!success)
{
removeRecording(recording);
}
else
{
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
}
/**
* Removes the temporary recording file from disk.
*
* @param recording to remove
*/
private void removeRecording(StreamableAudioRecording recording)
{
try
{
Files.delete(recording.getPath());
}
catch(IOException ioe)
{
mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString());
}
}
/**
* Broadcast configuration used by this broadcaster
*/
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
/**
* IAudioPacketListener interface method
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
/**
* Broadcasts the audio binary data.
*/
protected abstract void broadcastAudio(byte[] audio);
/**
* Broadcasts the next song's audio metadata prior to streaming the next song.
* @param metadata
*/
protected abstract void broadcastMetadata(AudioMetadata metadata);
/**
* Disconnects the broadcaster and halts all broadcasting.
*/
public void stop()
{
mEnabled = false;
mStreamManager.stop();
stopProcessor();
}
/**
* Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcastAudio
* handler for broadcastAudio to the remote server.
*/
public void receive(AudioPacket packet)
{
if(mEnabled)
{
mStreamManager.receive(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,
mRecordingQueueProcessor, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS );
}
}
}
/**
* Stops the audio queue processor
*/
private void stopProcessor()
{
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
}
/**
* Pauses or unpauses the broadcastAudio
*/
public void setPaused(boolean paused)
{
if(!getBroadcastState().isErrorState())
{
if(paused)
{
setBroadcastState(BroadcastState.PAUSED);
}
else
{
setBroadcastState(BroadcastState.READY);
}
}
}
/**
* Registers the listener to receive broadcastAudio state changes
*/
public void setListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventListener = listener;
}
/**
* Removes the listener from receiving broadcastAudio state changes
*/
public void removeListener()
{
mBroadcastEventListener = null;
}
/**
* Broadcasts the event to any registered listener
*/
public void broadcast(BroadcastEvent event)
{
if(mBroadcastEventListener != null)
{
mBroadcastEventListener.receive(event);
}
}
/**
* Sets the state of the broadcastAudio connection
*/
protected void setBroadcastState(BroadcastState state)
{
if(mBroadcastState != state)
{
mLog.debug("Changing State to: " + state);
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
}
}
/**
* Current state of the broadcastAudio connection
*/
public BroadcastState getBroadcastState()
{
return mBroadcastState;
}
public boolean connected()
{
return getBroadcastState() == BroadcastState.CONNECTED;
}
/**
* Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
*/
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
return state != BroadcastState.CONNECTED && !state.isErrorState();
}
/**
* Audio recording queue processor. Fetches recordings from the queue and chunks the recording byte content
* to subclass implementations for broadcast in the appropriate manner.
*/
public class RecordingQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private ByteArrayInputStream mInputStream;
private int mChunkSize;
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
if(mInputStream == null || mInputStream.available() <= 0)
{
nextRecording();
}
if(mInputStream != null)
{
int length = Math.min(mChunkSize, mInputStream.available());
byte[] audio = new byte[length];
try
{
mInputStream.read(audio);
broadcastAudio(audio);
}
catch(IOException ioe)
{
mLog.error("Error reading from in-memory audio recording input stream", ioe);
}
}
else
{
broadcastAudio(mSilenceFrame);
}
mProcessing.set(false);
}
}
/**
* Loads the next recording for broadcast
*/
private void nextRecording()
{
if(mInputStream != null)
{
mStreamedAudioCount++;
broadcast(new BroadcastEvent(AudioBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
}
mInputStream = null;
StreamableAudioRecording recording = mAudioRecordingQueue.poll();
if(recording != null)
{
try
{
byte[] audio = Files.readAllBytes(recording.getPath());
if(audio != null && audio.length > 0)
{
mInputStream = new ByteArrayInputStream(audio);
int wholeIntervalChunks = (int)(recording.getRecordingLength() / PROCESSOR_RUN_INTERVAL_MS);
//Check for divide by zero situation
if(wholeIntervalChunks == 0)
{
wholeIntervalChunks = 1;
}
mChunkSize = (int)(mInputStream.available() / wholeIntervalChunks) + 1;
broadcastMetadata(recording.getAudioMetadata());
}
else
{
broadcastMetadata(null);
}
}
catch(IOException ioe)
{
mLog.error("Error reading temporary audio stream recording [" + recording.getPath().toString() +
"] - skipping recording");
mInputStream = null;
}
removeRecording(recording);
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
else
{
broadcastMetadata(null);
}
}
}
}

View File

@@ -24,6 +24,7 @@ import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -31,17 +32,24 @@ import java.net.SocketAddress;
@XmlRootElement(name = "stream")
public abstract class BroadcastConfiguration
{
private BroadcastFormat mBroadcastFormat;
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
private String mName;
private String mHost;
private int mPort;
private String mPassword;
private long mDelay;
private boolean mEnabled = true;
public BroadcastConfiguration()
{
//No-arg constructor required for JAXB
}
/**
* Creates a copy/clone of the configuration
*/
public abstract BroadcastConfiguration copyOf();
/**
* Broadcast audio streaming configuration. Describes the streaming server and configuration details.
*/
@@ -57,7 +65,7 @@ public abstract class BroadcastConfiguration
public abstract BroadcastServerType getBroadcastServerType();
/**
* Name identifying this broadcast configuration.
* Name identifying this broadcastAudio configuration.
*/
@XmlAttribute(name = "name")
public String getName()
@@ -120,7 +128,7 @@ public abstract class BroadcastConfiguration
}
/**
* Port number of the streaming server to where the broadcast will be directed.
* Port number of the streaming server to where the broadcastAudio will be directed.
*
* @param port
*/
@@ -152,7 +160,7 @@ public abstract class BroadcastConfiguration
}
/**
* Sets the password used to authenticate the broadcast broadcast user to the streaming server
* Sets the password used to authenticate the broadcastAudio broadcastAudio user to the streaming server
*
* @param password
*/
@@ -170,11 +178,95 @@ public abstract class BroadcastConfiguration
}
/**
* Audio broadcast content type (e.g. mp3)
* Audio broadcastAudio content type (e.g. mp3)
*/
@XmlElement(name = "format")
@XmlAttribute(name = "format")
public BroadcastFormat getBroadcastFormat()
{
return mBroadcastFormat;
}
public void setBroadcastFormat(BroadcastFormat format)
{
mBroadcastFormat = format;
}
/**
* Audio broadcastAudio delay from recording start until the audio file is broadcastAudio to the server.
*/
@XmlAttribute(name = "delay")
public long getDelay()
{
return mDelay;
}
/**
* User specified delay in milli-seconds from audio call start until when the audio is broadcastAudio to the server.
* @param delay in milliseconds
*/
public void setDelay(long delay)
{
mDelay = delay;
}
/**
* Indicates if this broadcaster is enable, meaning that it will automatically connect on startup.
*/
@XmlAttribute(name = "enabled")
public boolean isEnabled()
{
return mEnabled;
}
public void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof BroadcastConfiguration))
{
return false;
}
BroadcastConfiguration that = (BroadcastConfiguration) o;
if (getBroadcastFormat() != that.getBroadcastFormat())
{
return false;
}
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null)
{
return false;
}
return getBroadcastServerType() == that.getBroadcastServerType();
}
@Override
public int hashCode()
{
int result = getBroadcastFormat().hashCode();
result = 31 * result + (getName() != null ? getName().hashCode() : 0);
result = 31 * result + getBroadcastServerType().hashCode();
return result;
}
/**
* Indicates if this configuration is valid. A minimal check will ensure that it contains at least a hostname and
* port number.
*/
@XmlTransient
public boolean isValid()
{
return mHost != null && mPort > 0;
}
}

View File

@@ -18,9 +18,10 @@
******************************************************************************/
package audio.broadcast;
public class BroadcastConfigurationEvent
public class BroadcastEvent
{
private BroadcastConfiguration mBroadcastConfiguration;
private AudioBroadcaster mAudioBroadcaster;
private Event mEvent;
/**
@@ -28,17 +29,44 @@ public class BroadcastConfigurationEvent
* @param configuration - alias that changed
* @param event - change event
*/
public BroadcastConfigurationEvent(BroadcastConfiguration configuration, Event event )
public BroadcastEvent(BroadcastConfiguration configuration, Event event )
{
mBroadcastConfiguration = configuration;
mEvent = event;
}
public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event )
{
mAudioBroadcaster = audioBroadcaster;
mEvent = event;
}
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
public AudioBroadcaster getAudioBroadcaster()
{
return mAudioBroadcaster;
}
/**
* Indicates if this event pertains to an audio broadcaster
*/
public boolean isAudioBroadcasterEvent()
{
return mAudioBroadcaster != null;
}
/**
* Indicates if this event pertains to a broadcast configuration
*/
public boolean isBroadcastConfigurationEvent()
{
return mBroadcastConfiguration != null;
}
public Event getEvent()
{
return mEvent;
@@ -49,8 +77,14 @@ public class BroadcastConfigurationEvent
*/
public enum Event
{
ADD,
CHANGE,
DELETE;
BROADCASTER_ADD,
BROADCASTER_QUEUE_CHANGE,
BROADCASTER_STATE_CHANGE,
BROADCASTER_STREAMED_COUNT_CHANGE,
BROADCASTER_DELETE,
CONFIGURATION_ADD,
CONFIGURATION_CHANGE,
CONFIGURATION_DELETE;
}
}

View File

@@ -18,27 +18,32 @@
******************************************************************************/
package audio.broadcast;
import audio.AudioPacket;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import audio.broadcast.icecast.IcecastHTTPConfiguration;
import audio.broadcast.icecast.IcecastHTTPBroadcaster;
import audio.broadcast.icecast.IcecastTCPBroadcaster;
import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
import audio.broadcast.icecast.IcecastTCPConfiguration;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v1.ShoutcastV1Broadcaster;
import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
import audio.broadcast.shoutcast.v2.ShoutcastV2Broadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
import audio.convert.IAudioConverter;
import audio.convert.MP3AudioConverter;
import controller.ThreadPoolManager;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.AudioRecorder;
import record.mp3.MP3Recorder;
import settings.SettingsManager;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class BroadcastFactory
{
@@ -54,34 +59,35 @@ public class BroadcastFactory
* @param configuration describing the server and audio types
* @return configured broadcaster or null
*/
public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
BroadcastConfiguration configuration)
public static AudioBroadcaster getBroadcaster(DefaultAsyncHttpClient defaultAsyncHttpClient,
ThreadPoolManager threadPoolManager,
BroadcastConfiguration configuration)
{
IAudioConverter converter = getAudioConverter(configuration);
if(converter != null)
if(configuration != null)
{
switch(configuration.getBroadcastServerType())
IAudioConverter converter = getAudioConverter(configuration);
if(converter != null)
{
case BROADCASTIFY:
return new IcecastTCPBroadcaster(threadPoolManager,
(BroadcastifyConfiguration)configuration, converter);
case ICECAST_HTTP:
return new IcecastHTTPBroadcaster(threadPoolManager,
(IcecastHTTPConfiguration)configuration, converter);
case ICECAST_TCP:
return new IcecastTCPBroadcaster(threadPoolManager,
(IcecastTCPConfiguration)configuration, converter);
case SHOUTCAST_V1:
return new ShoutcastV1Broadcaster(threadPoolManager,
(ShoutcastV1Configuration)configuration, converter);
case SHOUTCAST_V2:
return new ShoutcastV2Broadcaster(threadPoolManager,
(ShoutcastV2Configuration)configuration, converter);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcast configuration: " + configuration.getBroadcastFormat().name());
break;
switch(configuration.getBroadcastServerType())
{
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
(BroadcastifyConfiguration)configuration);
case ICECAST_TCP:
return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
(IcecastTCPConfiguration)configuration);
case ICECAST_HTTP:
return new IcecastHTTPAudioBroadcaster(threadPoolManager, (IcecastHTTPConfiguration)configuration);
case SHOUTCAST_V1:
return new ShoutcastV1AudioBroadcaster(threadPoolManager, (ShoutcastV1Configuration)configuration);
case SHOUTCAST_V2:
return new ShoutcastV2AudioBroadcaster(threadPoolManager, (ShoutcastV2Configuration)configuration);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
break;
}
}
}
@@ -100,14 +106,14 @@ public class BroadcastFactory
case MP3:
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
default:
mLog.info("Unrecognized broadcast format: " + configuration.getBroadcastFormat().name());
mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
}
return null;
}
/**
* Creates a broadcast configuration for the specified server type and format
* Creates a broadcastAudio configuration for the specified server type and format
*
* @param serverType for the configuration
* @param format for the output audio format
@@ -129,7 +135,7 @@ public class BroadcastFactory
return new ShoutcastV2Configuration(format);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcast server type: " + serverType.name());
mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
break;
}
@@ -137,20 +143,21 @@ public class BroadcastFactory
}
/**
* Constructs an editor for the specified broadcast configuration
* Constructs an editor for the specified broadcastAudio configuration
* @param configuration to modify or view
* @param broadcastModel model for broadcast configurations
* @return an editor for the specified broadcast configuration
* @param broadcastModel model for broadcastAudio configurations
* @return an editor for the specified broadcastAudio configuration
*/
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
BroadcastModel broadcastModel)
BroadcastModel broadcastModel,
SettingsManager settingsManager)
{
Editor<BroadcastConfiguration> editor;
switch (configuration.getBroadcastServerType())
{
case BROADCASTIFY:
editor = new BroadcastifyConfigurationEditor(broadcastModel);
editor = new BroadcastifyConfigurationEditor(broadcastModel, settingsManager);
break;
default:
editor = new EmptyEditor<BroadcastConfiguration>();
@@ -163,7 +170,7 @@ public class BroadcastFactory
}
/**
* Creates an audio recorder for the specified broadcast format using the specified path output file name
* Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
*/
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
{
@@ -172,8 +179,28 @@ public class BroadcastFactory
case MP3:
return new MP3Recorder(path);
default:
mLog.debug("Unrecognized broadcast format [" + broadcastFormat + "] cannot create audio recorder");
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
return null;
}
}
public static byte[] getSilenceFrame(BroadcastFormat format, long duration)
{
int length = (int)(duration * 8); //8000 Hz sample rate
float[] silence = new float[length];
AudioPacket silencePacket = new AudioPacket(silence, null);
List<AudioPacket> silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
switch(format)
{
case MP3:
MP3AudioConverter converter = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE,
MP3Recorder.CONSTANT_BIT_RATE);
return converter.convert(silencePackets);
default:
throw new IllegalArgumentException("Unrecognized broadcast format: " + format.name());
}
}
}

View File

@@ -28,6 +28,7 @@ public enum BroadcastFormat
private BroadcastFormat(String value, String fileExtension)
{
mValue = value;
mFileExtension = fileExtension;
}
public String getValue()

View File

@@ -18,28 +18,70 @@
******************************************************************************/
package audio.broadcast;
import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket;
import controller.ThreadPoolManager;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Broadcaster;
import sample.Listener;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BroadcastModel extends AbstractTableModel
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
{
private static final int COLUMN_CHANNEL_NAME = 0;
private static final int COLUMN_SERVER_TYPE = 1;
private static final int COLUMN_SERVER_ADDRESS = 2;
private final static Logger mLog = LoggerFactory.getLogger( BroadcastModel.class );
public static final String TEMPORARY_STREAM_DIRECTORY = "streaming";
public static final String TEMPORARY_STREAM_FILE_SUFFIX = "temporary_streaming_file_";
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
private static final int COLUMN_SERVER_ICON = 0;
private static final int COLUMN_STREAM_NAME = 1;
private static final int COLUMN_BROADCASTER_STATUS = 2;
private static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
private static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
private sample.Broadcaster<BroadcastConfigurationEvent> mBroadcastEventBroadcaster = new sample.Broadcaster<>();
public BroadcastModel()
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
private ThreadPoolManager mThreadPoolManager;
private SettingsManager mSettingsManager;
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
/**
* Model for managing Broadcast configurations and any associated broadcaster instances.
*/
public BroadcastModel(ThreadPoolManager threadPoolManager, SettingsManager settingsManager)
{
mThreadPoolManager = threadPoolManager;
mSettingsManager = settingsManager;
removeOrphanedTemporaryRecordings();
}
/**
* List of broadcast configuration names
* List of broadcastAudio configuration names
*/
public List<String> getBroadcastConfigurationNames()
{
@@ -54,7 +96,7 @@ public class BroadcastModel extends AbstractTableModel
}
/**
* Current list of broadcast configurations
* Current list of broadcastAudio configurations
*/
public List<BroadcastConfiguration> getBroadcastConfigurations()
{
@@ -62,7 +104,7 @@ public class BroadcastModel extends AbstractTableModel
}
/**
* Adds the list of broadcast configurations to this model
* Adds the list of broadcastAudio configurations to this model
*/
public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations)
{
@@ -73,58 +115,278 @@ public class BroadcastModel extends AbstractTableModel
}
/**
* Adds the broadcast configuration to this model
* Adds the broadcastAudio configuration to this model
*/
public void addBroadcastConfiguration(BroadcastConfiguration configuration)
{
if(configuration != null)
{
mBroadcastConfigurations.add(configuration);
ensureUniqueName(configuration);
int index = mBroadcastConfigurations.size() - 1;
if(!mBroadcastConfigurations.contains(configuration))
{
mBroadcastConfigurations.add(configuration);
fireTableRowsInserted( index, index );
int index = mBroadcastConfigurations.size() - 1;
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.ADD ) );
}
}
fireTableRowsInserted( index, index );
public void removeBroadcastConfiguration(BroadcastConfiguration configuration)
{
if(configuration != null && mBroadcastConfigurations.contains(configuration))
{
int index = mBroadcastConfigurations.indexOf(configuration);
mBroadcastConfigurationMap.put(configuration.getName(), configuration);
mBroadcastConfigurations.remove(configuration);
fireTableRowsDeleted( index, index );
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.DELETE ) );
process( new BroadcastEvent( configuration, BroadcastEvent.Event.CONFIGURATION_ADD) );
}
}
}
/**
* Registers the listener to receive broadcast configuration events
* Clones the configuration and adds it this model with a unique configuration name
*/
public void addListener(Listener<BroadcastConfigurationEvent> listener)
public BroadcastConfiguration cloneBroadcastConfiguration(BroadcastConfiguration configuration)
{
if(configuration != null)
{
BroadcastConfiguration clone = configuration.copyOf();
addBroadcastConfiguration(clone);
return clone;
}
return null;
}
/**
* Updates the configuration's name so that it is unique among all other broadcast configurations
* @param configuration
*/
private void ensureUniqueName(BroadcastConfiguration configuration)
{
if(configuration.getName() == null || configuration.getName().isEmpty())
{
configuration.setName("New Configuration");
}
while(!isUniqueName(configuration.getName(), configuration))
{
String currentName = configuration.getName();
if(currentName.matches(UNIQUE_NAME_REGEX))
{
int currentVersion = 1;
StringBuilder sb = new StringBuilder();
Matcher m = Pattern.compile(UNIQUE_NAME_REGEX).matcher(currentName);
if(m.find())
{
String version = m.group(2);
try
{
currentVersion = Integer.parseInt(version);
}
catch(Exception e)
{
//Couldn't parse the version number -- keep incrementing until we find a winner
}
currentVersion++;
sb.append(m.group(1)).append("(").append(currentVersion).append(")");
}
else
{
sb.append(configuration.getName()).append("(").append(currentVersion).append(")");
}
configuration.setName(sb.toString());
}
else
{
StringBuilder sb = new StringBuilder();
sb.append(configuration.getName()).append("(2)");
configuration.setName(sb.toString());
}
}
}
/**
* Indicates if the name is unique among all of the broadcast configurations. Checks each of the configurations
* in this model for a name collision. Assumes that the name argument will be assigned to the configuration
* argument if the name is unique, therefore, the name will not be checked against the configuration argument for
* a collision.
*
* @param name to check for uniqueness
* @param configuration to ignore when checking the name for uniqueness
* @return true if the name is not null and unique among all configurations managed by this model
*/
public boolean isUniqueName(String name, BroadcastConfiguration configuration)
{
if(name == null || name.isEmpty())
{
return false;
}
for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
{
if(configurationToCompare != configuration && configurationToCompare.getName().equals(name))
{
return false;
}
}
return true;
}
public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
{
if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
{
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
mBroadcastConfigurations.remove(broadcastConfiguration);
mBroadcastConfigurationMap.remove(broadcastConfiguration.getName());
broadcast( new BroadcastEvent( broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE) );
fireTableRowsDeleted( index, index );
}
}
/**
* Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
*/
public AudioBroadcaster getBroadcaster(String streamName)
{
return mBroadcasterMap.get(streamName);
}
@Override
public void receive(AudioPacket audioPacket)
{
if (audioPacket.hasAudioMetadata() && audioPacket.getAudioMetadata().isStreamable())
{
for (BroadcastChannel broadcastChannel : audioPacket.getAudioMetadata().getBroadcastChannels())
{
String channelName = broadcastChannel.getChannelName();
if (channelName != null)
{
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if (audioBroadcaster != null)
{
audioBroadcaster.receive(audioPacket);
}
}
}
}
}
/**
* Creates a new broadcaster for the broadcast configuration and adds it to the model
*/
private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
{
if (broadcastConfiguration != null &&
broadcastConfiguration.isEnabled() &&
broadcastConfiguration.isValid() &&
!mBroadcasterMap.containsKey(broadcastConfiguration.getName()))
{
AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(getDefaultAsyncHttpClient(),
mThreadPoolManager, broadcastConfiguration);
if (audioBroadcaster != null)
{
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
process(broadcastEvent);
}
});
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
{
fireTableRowsUpdated(index, index);
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
}
}
}
/**
* Shut down a broadcaster created from the configuration and remove it from this model
*/
private void deleteBroadcaster(String name)
{
if(name != null && mBroadcasterMap.containsKey(name))
{
AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(name);
if(audioBroadcaster != null)
{
audioBroadcaster.stop();
audioBroadcaster.removeListener();
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
{
fireTableRowsUpdated(index, index);
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
}
}
}
private DefaultAsyncHttpClient getDefaultAsyncHttpClient()
{
if (mDefaultAsyncHttpClient == null)
{
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
}
return mDefaultAsyncHttpClient;
}
/**
* Returns the broadcast configuration identified by the stream name
*/
public BroadcastConfiguration getBroadcastConfiguration(String streamName)
{
return mBroadcastConfigurationMap.get(streamName);
}
/**
* Registers the listener to receive broadcastAudio configuration events
*/
public void addListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving broadcast configuration events
* Removes the listener from receiving broadcastAudio configuration events
*/
public void removeListener(Listener<BroadcastConfigurationEvent> listener)
public void removeListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventBroadcaster.removeListener(listener);
}
/**
* Broadcasts the broadcast configuration change event
* Broadcasts the broadcastAudio configuration change event
*/
public void broadcast( BroadcastConfigurationEvent event )
public void broadcast( BroadcastEvent event )
{
if( event.getEvent() == BroadcastConfigurationEvent.Event.CHANGE )
if( event.getEvent() == BroadcastEvent.Event.CONFIGURATION_CHANGE)
{
int index = mBroadcastConfigurations.indexOf( event.getBroadcastConfiguration() );
@@ -134,6 +396,88 @@ public class BroadcastModel extends AbstractTableModel
mBroadcastEventBroadcaster.broadcast( event );
}
/**
* Process a broadcast event from one of the broadcasters managed by this model
*/
public void process(BroadcastEvent broadcastEvent)
{
if(broadcastEvent.isBroadcastConfigurationEvent())
{
switch(broadcastEvent.getEvent())
{
case CONFIGURATION_ADD:
createBroadcaster(broadcastEvent.getBroadcastConfiguration());
break;
case CONFIGURATION_CHANGE:
//Delete and recreate the broadcaster for any broadcast configuration changes
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
cleanupMapAssociations(broadcastConfiguration);
createBroadcaster(broadcastConfiguration);
break;
case CONFIGURATION_DELETE:
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
break;
}
}
else if(broadcastEvent.isAudioBroadcasterEvent())
{
int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
switch(broadcastEvent.getEvent())
{
case BROADCASTER_QUEUE_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_QUEUE_SIZE);
}
break;
case BROADCASTER_STATE_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_STATUS);
}
break;
case BROADCASTER_STREAMED_COUNT_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
}
break;
}
}
//Rebroadcast the event to any listeners of this model
broadcast(broadcastEvent);
}
/**
* When the name of a broadcast configuration changes, remove the old map associations so that they can be replaced
* with the new map associations. Remove any broadcaster that is associated with the old configuration name.
*
* @param broadcastConfiguration
*/
private void cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
{
String oldName = null;
for(Map.Entry<String,BroadcastConfiguration> entry: mBroadcastConfigurationMap.entrySet())
{
if(entry.getValue() == broadcastConfiguration)
{
oldName = entry.getKey();
continue;
}
}
if(oldName != null)
{
deleteBroadcaster(oldName);
mBroadcastConfigurationMap.remove(oldName);
}
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
}
@Override
public int getRowCount()
{
@@ -143,7 +487,7 @@ public class BroadcastModel extends AbstractTableModel
@Override
public int getColumnCount()
{
return 3;
return 5;
}
@Override
@@ -157,12 +501,47 @@ public class BroadcastModel extends AbstractTableModel
{
switch(columnIndex)
{
case COLUMN_CHANNEL_NAME:
case COLUMN_SERVER_ICON:
String iconName = configuration.getBroadcastServerType().getIconName();
if(iconName != null && mSettingsManager != null)
{
return mSettingsManager.getImageIcon(iconName, 14);
}
break;
case COLUMN_STREAM_NAME:
return configuration.getName();
case COLUMN_SERVER_TYPE:
return configuration.getBroadcastServerType().toString();
case COLUMN_SERVER_ADDRESS:
return configuration.getHost();
case COLUMN_BROADCASTER_STATUS:
AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterA != null)
{
return audioBroadcasterA.getBroadcastState().toString();
}
else if(!configuration.isEnabled())
{
return "Disabled";
}
else if(!configuration.isValid())
{
return "Invalid Settings";
}
case COLUMN_BROADCASTER_QUEUE_SIZE:
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterB != null)
{
return audioBroadcasterB.getQueueSize();
}
break;
case COLUMN_BROADCASTER_STREAMED_COUNT:
AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterC != null)
{
return audioBroadcasterC.getStreamedAudioCount();
}
break;
default:
break;
}
@@ -172,6 +551,17 @@ public class BroadcastModel extends AbstractTableModel
return null;
}
@Override
public Class<?> getColumnClass(int columnIndex)
{
if(columnIndex == COLUMN_SERVER_ICON)
{
return ImageIcon.class;
}
return String.class;
}
/**
* Broadcast configuration at the specified model row
*/
@@ -193,14 +583,60 @@ public class BroadcastModel extends AbstractTableModel
{
switch(column)
{
case COLUMN_CHANNEL_NAME:
return "Channel";
case COLUMN_SERVER_TYPE:
return "Server";
case COLUMN_SERVER_ADDRESS:
return "Address";
case COLUMN_SERVER_ICON:
return null;
case COLUMN_STREAM_NAME:
return "Name";
case COLUMN_BROADCASTER_STATUS:
return "Status";
case COLUMN_BROADCASTER_QUEUE_SIZE:
return "Queued";
case COLUMN_BROADCASTER_STREAMED_COUNT:
return "Streamed";
}
return null;
}
/**
* Removes any temporary stream recordings left-over from the previous application run.
*
* This should only be invoked on startup.
*/
private void removeOrphanedTemporaryRecordings()
{
Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
if(path != null && Files.isDirectory(path))
{
StringBuilder sb = new StringBuilder();
sb.append(TEMPORARY_STREAM_FILE_SUFFIX);
sb.append("*.*");
try(DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path, sb.toString()))
{
directoryStream.forEach(new Consumer<Path>()
{
@Override
public void accept(Path path)
{
mLog.info("Deleting orphaned temporary stream recording: " + path.toString());
try
{
Files.delete(path);
}
catch(IOException ioe)
{
mLog.error("Couldn't delete orphaned temporary recording: " + path.toString(), ioe);
}
}
});
}
catch(IOException ioe)
{
mLog.error("Error discovering orphaned temporary stream recording files", ioe);
}
}
}
}

View File

@@ -18,15 +18,13 @@
******************************************************************************/
package audio.broadcast;
import alias.Alias;
import alias.AliasFactory;
import alias.id.AliasID;
import alias.id.AliasIDType;
import alias.id.AliasIdentifierEditor;
import com.jidesoft.swing.JideSplitPane;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
@@ -38,22 +36,27 @@ import java.awt.event.MouseEvent;
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
{
private static final String NEW_ALIAS = "New";
private static final String COPY_ALIAS = "Copy";
private static final String DELETE_ALIAS = "Delete";
private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
private static final String NEW_BROADCAST_CONFIGURATION = "New";
private static final String COPY_BROADCAST_CONFIGURATION = "Copy";
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
private BroadcastModel mBroadcastModel;
private SettingsManager mSettingsManager;
private JTable mBroadcastConfigurationTable;
private JideSplitPane mSplitPane;
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
private Editor<BroadcastConfiguration> mEditor;
private JButton mNewButton = new JButton(NEW_ALIAS);
private JButton mCopyButton = new JButton(COPY_ALIAS);
private JButton mDeleteButton = new JButton(DELETE_ALIAS);
private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
public BroadcastPanel(BroadcastModel broadcastModel)
public BroadcastPanel(BroadcastModel broadcastModel, SettingsManager settingManager)
{
mBroadcastModel = broadcastModel;
mSettingsManager = settingManager;
mEditor = mEmptyEditor;
init();
@@ -68,6 +71,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setWidth(15);
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
@@ -75,7 +79,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
mNewButton = new JButton("New ...");
mNewButton.setToolTipText("Create a new broadcast configuration");
mNewButton.setToolTipText("Create a new broadcastAudio configuration");
mNewButton.addMouseListener(new MouseAdapter()
{
@Override
@@ -121,18 +125,62 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
@Override
public void actionPerformed(ActionEvent e)
{
switch(e.getActionCommand())
{
case COPY_BROADCAST_CONFIGURATION:
BroadcastConfiguration copySelected = getSelectedBroadcastConfiguration();
mBroadcastModel.cloneBroadcastConfiguration(copySelected);
break;
case DELETE_BROADCAST_CONFIGURATION:
BroadcastConfiguration deleteSelected = getSelectedBroadcastConfiguration();
if(deleteSelected != null)
{
int choice = JOptionPane.showConfirmDialog(BroadcastPanel.this, "Do you want to delete broadcast\n" +
" configuration " + deleteSelected.getName() + "?", "Delete Broadcast Configuration?",
JOptionPane.YES_NO_OPTION);
if(choice == JOptionPane.YES_OPTION)
{
mBroadcastModel.removeBroadcastConfiguration(deleteSelected);
}
}
break;
}
}
/**
* Returns the selected configuration
*/
private BroadcastConfiguration getSelectedBroadcastConfiguration()
{
int viewRow = mBroadcastConfigurationTable.getSelectedRow();
if(viewRow >= 0)
{
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(viewRow);
if(modelRow >= 0)
{
return mBroadcastModel.getConfigurationAt(modelRow);
}
}
return null;
}
@Override
public void valueChanged(ListSelectionEvent e)
{
if (!e.getValueIsAdjusting())
{
int row = mBroadcastConfigurationTable.getSelectedRow();
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(row);
BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getConfigurationAt(modelRow);
//Enable buttons accordingly
mCopyButton.setEnabled(selectedConfiguration != null);
mDeleteButton.setEnabled(selectedConfiguration != null);
if (mEditor.isModified())
{
@@ -153,7 +201,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
if (selectedConfiguration != null)
{
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel);
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mSettingsManager);
}
else
{
@@ -186,6 +234,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
final BroadcastConfiguration configuration =
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
mBroadcastModel.addBroadcastConfiguration(configuration);
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);

View File

@@ -20,36 +20,29 @@ package audio.broadcast;
public enum BroadcastServerType
{
/**
* Broadcastify is currently using Icecast Server 2.3.2
*/
BROADCASTIFY("Broadcastify"),
/**
* Icecast Server 2.4.x and later
*/
ICECAST_HTTP("Icecast 2.4+"),
/**
* Icecast Server 2.3.x
*/
ICECAST_TCP("Icecast 2.3"),
SHOUTCAST_V1("Shoutcast 1.x"),
SHOUTCAST_V2("Shoutcast 2.x"),
UNKNOWN("Unknown");
BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
ICECAST_HTTP("Icecast 2.4+", null),
ICECAST_TCP("Icecast 2.3", null),
SHOUTCAST_V1("Shoutcast 1.x", null),
SHOUTCAST_V2("Shoutcast 2.x", null),
UNKNOWN("Unknown", null);
private String mLabel;
private String mIconName;
BroadcastServerType(String label)
BroadcastServerType(String label, String iconName)
{
mLabel = label;
mIconName = iconName;
}
public String toString()
{
return mLabel;
}
public String getIconName()
{
return mIconName;
}
}

View File

@@ -26,7 +26,7 @@ public enum BroadcastState
BROADCAST_ERROR("Broadcast Error", false),
/**
* Connected to the broadcast server and capable of streaming
* Connected to the broadcastAudio server and capable of streaming
*/
CONNECTED("Connected", false),
@@ -36,7 +36,7 @@ public enum BroadcastState
CONNECTING("Connecting", false),
/**
* Disconnected from the broadcast server
* Disconnected from the broadcastAudio server
*/
DISCONNECTED("Disconnected", true),

View File

@@ -1,242 +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.broadcast;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.convert.IAudioConverter;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class Broadcaster implements IAudioPacketListener, Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger( Broadcaster.class );
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture mScheduledTask;
private AudioQueueProcessor mAudioQueueProcessor;
protected LinkedTransferQueue<AudioPacket> mAudioQueue = new LinkedTransferQueue<>();
private List<Listener<BroadcastState>> mBroadcastStateListeners = new CopyOnWriteArrayList<>();
private BroadcastState mBroadcastState = BroadcastState.READY;
private BroadcastConfiguration mBroadcastConfiguration;
private IAudioConverter mAudioConverter;
private boolean mEnabled = true;
/**
* Broadcaster for sending audio to a remote broadcast server.
*/
public Broadcaster(ThreadPoolManager threadPoolManager,
BroadcastConfiguration broadcastConfiguration,
IAudioConverter audioConverter)
{
mThreadPoolManager = threadPoolManager;
mBroadcastConfiguration = broadcastConfiguration;
mAudioConverter = audioConverter;
mAudioQueueProcessor = new AudioQueueProcessor();
}
/**
* Broadcast configuration used by this broadcaster
*/
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
/**
* IAudioPacketListener interface method
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
/**
* Audio converter used by this broadcaster to convert PCM audio packets to the desired output format.
*/
protected IAudioConverter getAudioConverter()
{
return mAudioConverter;
}
/**
* Commands the broadcaster to broadcast any queued audio packets. This method is invoked by a scheduled thread
* pool task to periodically command the sub-class to broadcast queued audio packets.
*/
protected abstract void broadcast();
/**
* Disconnects the broadcaster and halts all broadcasting.
*/
public void stop()
{
mEnabled = false;
stopProcessor();
}
/**
* 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, 250, TimeUnit.MILLISECONDS );
}
}
}
/**
* Stops the audio queue processor
*/
private void stopProcessor()
{
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
}
/**
* 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;
}
/**
* Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
*/
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
return state != BroadcastState.CONNECTED && !state.isErrorState();
}
/**
* Audio packet queue processor. Runs via a scheduled thread pool task as a timing processor to instruct
* sub-classes to broadcast queued audio packets. Internal state monitor ensures that subsequent invocations of
* the broadcast method do not start until the previous invocation completes.
*/
public class AudioQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
broadcast();
mProcessing.set(false);
}
}
}
}

View File

@@ -39,8 +39,7 @@ public class StreamManager implements Listener<AudioPacket>
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
private ThreadPoolManager mThreadPoolManager;
private BroadcastFormat mBroadcastFormat;
private Listener<StreamableAudioRecording> mOutputListener;
private AudioBroadcaster mAudioBroadcaster;
private long mDelay;
private Path mTempDirectory;
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
@@ -57,24 +56,17 @@ public class StreamManager implements Listener<AudioPacket>
* has elapsed.
*
* @param threadPoolManager for scheduling runnables
* @param broadcastFormat defines the output recording format
* @param outputListener to receive completed streamable audio recordings
* @param audioBroadcaster to receive completed streamable audio recordings
* @param delay from recording start until audio recording is nominated to output listener
* @param tempDirectory where to store temporary audio recordings
*/
public StreamManager(ThreadPoolManager threadPoolManager,
BroadcastFormat broadcastFormat,
Listener<StreamableAudioRecording> outputListener,
long delay,
Path tempDirectory)
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, long delay, Path tempDirectory)
{
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
assert(mThreadPoolManager != null);
assert(mBroadcastFormat != null);
mThreadPoolManager = threadPoolManager;
mBroadcastFormat = broadcastFormat;
mOutputListener = outputListener;
mAudioBroadcaster = audioBroadcaster;
mDelay = delay;
mTempDirectory = tempDirectory;
}
@@ -142,7 +134,7 @@ public class StreamManager implements Listener<AudioPacket>
else
{
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
mBroadcastFormat);
mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat());
recorder.start(mThreadPoolManager.getScheduledExecutorService());
recorder.receive(audioPacket);
mStreamRecorders.put(sourceChannelID, recorder);
@@ -176,11 +168,11 @@ public class StreamManager implements Listener<AudioPacket>
recorder.stop();
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
recorder.getMetadata());
recorder.getMetadata(), recorder.getRecordingLength());
if(mDelay <= 0)
{
mOutputListener.receive(streamableAudioRecording);
mAudioBroadcaster.receive(streamableAudioRecording);
}
else
{
@@ -198,9 +190,9 @@ public class StreamManager implements Listener<AudioPacket>
private Path getTemporaryRecordingPath()
{
StringBuilder sb = new StringBuilder();
sb.append("temporary_streaming_file_");
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
sb.append( TimeStamp.getLongTimeStamp( "_" ) );
sb.append( mBroadcastFormat.getFileExtension() );
sb.append( mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat().getFileExtension() );
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
@@ -222,9 +214,9 @@ public class StreamManager implements Listener<AudioPacket>
@Override
public void run()
{
if(mOutputListener != null)
if(mAudioBroadcaster != null)
{
mOutputListener.receive(mStreamableAudioRecording);
mAudioBroadcaster.receive(mStreamableAudioRecording);
}
}
}

View File

@@ -26,11 +26,19 @@ public class StreamableAudioRecording
{
private Path mPath;
private AudioMetadata mAudioMetadata;
private long mRecordingLengthMilliSeconds;
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata)
/**
* Audio recording that is ready to be streamed
* @param path to the audio recording file
* @param audioMetadata associated with the recording
* @param recordingLength in milliseconds
*/
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long recordingLength)
{
mPath = path;
mAudioMetadata = audioMetadata;
mRecordingLengthMilliSeconds = recordingLength;
}
/**
@@ -48,4 +56,12 @@ public class StreamableAudioRecording
{
return mAudioMetadata;
}
/**
* Recording length in milliseconds
*/
public long getRecordingLength()
{
return mRecordingLengthMilliSeconds;
}
}

View File

@@ -18,6 +18,7 @@
******************************************************************************/
package audio.broadcast.broadcastify;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.icecast.IcecastTCPConfiguration;
@@ -26,11 +27,12 @@ import javax.xml.bind.annotation.XmlAttribute;
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
{
public int mFeedID;
private int mFeedID;
public BroadcastifyConfiguration()
{
//No-arg constructor for JAXB
this(BroadcastFormat.MP3);
}
/**
@@ -47,6 +49,34 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
setSampleRate(8000);
}
@Override
public BroadcastConfiguration copyOf()
{
BroadcastifyConfiguration copy = new BroadcastifyConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
//Broadcastify Configuration Parameters
copy.setFeedID(getFeedID());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
@@ -63,4 +93,10 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
{
mFeedID = feedID;
}
@Override
public boolean isValid()
{
return super.isValid() && (mFeedID > 0);
}
}

View File

@@ -19,10 +19,14 @@
package audio.broadcast.broadcastify;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEvent;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import java.awt.*;
@@ -31,6 +35,9 @@ import java.awt.event.ActionListener;
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
private SettingsManager mSettingsManager;
private BroadcastModel mBroadcastModel;
private JTextField mFeedID;
private JTextField mName;
@@ -40,24 +47,27 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
private JTextField mPassword;
private JButton mSaveButton;
private JButton mResetButton;
private JCheckBox mEnabled;
public BroadcastifyConfigurationEditor(BroadcastModel model)
public BroadcastifyConfigurationEditor(BroadcastModel model, SettingsManager settingsManager)
{
mBroadcastModel = model;
mSettingsManager = settingsManager;
init();
}
private void init()
{
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][][grow,fill]" ) );
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][grow,fill]" ) );
setPreferredSize(new Dimension(150,400));
add(new JLabel("Broadcastify Channel"), "span, align center");
JButton selectButton = new JButton("Select ...");
add(selectButton, "wrap");
JLabel channelLabel = new JLabel("Broadcastify Stream");
add(new JSeparator(JSeparator.HORIZONTAL), "span,growx");
ImageIcon broadcastifyIcon = mSettingsManager.getImageIcon(BroadcastServerType.BROADCASTIFY.getIconName(), 25);
channelLabel.setIcon(broadcastifyIcon);
add(channelLabel, "span, align center");
add(new JLabel("Feed ID:"));
mFeedID = new JTextField();
@@ -89,6 +99,17 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mPassword.getDocument().addDocumentListener(this);
add(mPassword);
mEnabled = new JCheckBox("Enabled");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled, "span");
mSaveButton = new JButton("Save");
mSaveButton.setEnabled(false);
mSaveButton.addActionListener(new ActionListener()
@@ -139,6 +160,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mMountPoint.setText(config.getMountPoint());
mPassword.setText(config.getPassword());
mEnabled.setSelected(config.isEnabled());
}
else
{
@@ -148,6 +170,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mPort.setText(null);
mMountPoint.setText(null);
mPassword.setText(null);
mEnabled.setSelected(false);
}
setModified(false);
@@ -158,14 +181,151 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
{
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
if(validateConfiguration())
{
config.setName(mName.getText());
config.setFeedID(getFeedID());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
config.setPassword(mPassword.getText());
config.setEnabled(mEnabled.isSelected());
setModified(false);
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
}
}
/**
* Validates the user specified values in the controls prior to saving them to the configuration
*/
private boolean validateConfiguration()
{
String name = mName.getText();
//A unique stream name is required
if(!mBroadcastModel.isUniqueName(name, getItem()))
{
String message = (name == null || name.isEmpty()) ?
"Please specify a stream name." : "Stream name " + name +
"is already in use.\nPlease choose another name.";
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this,
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
mName.requestFocus();
return false;
}
//Feed ID is optional, but required
if(!validateIntegerTextField(mFeedID, "Invalid Feed ID", "Please specify a feed ID", 1, Integer.MAX_VALUE))
{
return true;
}
//Server address is optional, but required
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
{
return true;
}
//Port is optional, but required
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
{
return true;
}
//Mount Point is optional, but required
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
{
return true;
}
//Password is optional, but required
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
{
return true;
}
return true;
}
/**
* Validates a text field control for a non-null, non-empty value
* @param field to validate
* @param title to use for error dialog
* @param message to use for error dialog
* @return true if field contains a non-null, non-empty value
*/
private boolean validateTextField(JTextField field, String title, String message)
{
String text = field.getText();
if(text == null || text.isEmpty())
{
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
JOptionPane.ERROR_MESSAGE);
field.requestFocus();
return false;
}
return true;
}
/**
* Validates the text field control that contains an integer value for non-null, non-empty and within the
* specified min/max valid range.
* @param field to validate
* @param title to use for error dialog
* @param message to use for error dialog
* @param minValid value
* @param maxValid value
* @return true if field contains a non-null, non-empty value within the valid min/max range
*/
private boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
{
if(validateTextField(field, title, message))
{
String text = field.getText();
try
{
int value = Integer.parseInt(text);
if(minValid <= value && value <= maxValid)
{
return true;
}
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
JOptionPane.ERROR_MESSAGE);
field.requestFocus();
}
return false;
}
/**
* Parses an Integer from the feed ID text control or returns 0 if the control doesn't contain an integer value.
*/
private int getFeedID()
{
String feedID = mFeedID.getText();
if(feedID != null && !feedID.isEmpty())
{
try
{
int id = Integer.parseInt(feedID);
config.setFeedID(id);
return Integer.parseInt(feedID);
}
catch(Exception e)
{
@@ -173,17 +333,21 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
}
}
config.setName(mName.getText());
config.setHost(mServer.getText());
return 0;
}
/**
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
*/
private int getPort()
{
String port = mPort.getText();
if(port != null && !port.isEmpty())
{
try
{
int number = Integer.parseInt(port);
config.setPort(number);
return Integer.parseInt(port);
}
catch(Exception e)
{
@@ -191,11 +355,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
}
}
config.setMountPoint(mMountPoint.getText());
config.setPassword(mPassword.getText());
setModified(false);
mBroadcastModel.broadcast(new BroadcastConfigurationEvent(getItem(), BroadcastConfigurationEvent.Event.CHANGE));
return 0;
}
}

View File

@@ -19,11 +19,11 @@
package audio.broadcast.icecast;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory;
import audio.convert.IAudioConverter;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder;
@@ -53,9 +53,9 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<ByteBuffer>
public class IcecastHTTPAudioBroadcaster extends AudioBroadcaster implements Publisher<ByteBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPBroadcaster.class );
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPAudioBroadcaster.class );
private static final String HTTP_100_CONTINUE = "100-continue";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
@@ -69,15 +69,13 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
/**
* Creates an Icecast 2.4.x (or newer) compatible broadcaster using HTTP 1.1 protocol
*
* Note: use @see IcecastTCPBroadcaster for Icecast version 2.3.x and older.
* Note: use @see IcecastTCPAudioBroadcaster for Icecast version 2.3.x and older.
* @param configuration
* @param audioConverter
*/
public IcecastHTTPBroadcaster(ThreadPoolManager threadPoolManager,
IcecastHTTPConfiguration configuration,
IAudioConverter audioConverter)
public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
{
super(threadPoolManager, configuration, audioConverter);
super(threadPoolManager, configuration);
}
/**
@@ -88,31 +86,24 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to send metadata to Icecast HTTP server - needs code!");
}
/**
* Broadcast any queued audio packets using Icecast V2 Protocol
*/
@Override
public void broadcast()
protected void broadcastAudio(byte[] audio)
{
if(connect())
{
if (mSubscription != null && mSubscription.hasDemand())
{
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
if (!mPacketsToBroadcast.isEmpty())
{
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mLog.debug("We have [" + mPacketsToBroadcast.size() +
"] packets - sending:" + convertedAudio.length + " bytes");
ByteBuffer buffer = ByteBuffer.wrap(convertedAudio);
mSubscription.broadcast(buffer);
mPacketsToBroadcast.clear();
}
mSubscription.broadcast(ByteBuffer.wrap(audio));
}
}
}
@@ -418,8 +409,9 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
else
{
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
@@ -432,7 +424,7 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.setListener(audioBroadcaster);
reader.read();
}
catch (IOException e)

View File

@@ -36,6 +36,11 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
private int mSampleRate = 8000;
private String mURL;
public IcecastHTTPConfiguration()
{
//no-arg JAXB constructor
}
/**
* Icecast 2.3.x and 2.4.x compatible configuration
* @param format
@@ -45,6 +50,33 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setBitRate(getBitRate());
copy.setChannels(getChannels());
copy.setSampleRate(getSampleRate());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
@@ -149,7 +181,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Public visibility of the broadcast
* Public visibility of the broadcastAudio
*/
public boolean isPublic()
{
@@ -157,8 +189,8 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
@@ -166,7 +198,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Number of audio channels in the broadcast
* Number of audio channels in the broadcastAudio
*/
public int getChannels()
{
@@ -174,7 +206,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Sets the number of audio channels in the broadcast
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
@@ -224,7 +256,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
*/
public String getURL()
{
@@ -232,7 +264,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)

View File

@@ -18,16 +18,24 @@
******************************************************************************/
package audio.broadcast.icecast;
import alias.Alias;
import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory;
import audio.convert.IAudioConverter;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Realm;
import org.asynchttpclient.Response;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,6 +45,7 @@ import record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
@@ -44,91 +53,186 @@ import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastTCPBroadcaster extends Broadcaster
public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPBroadcaster.class );
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPAudioBroadcaster.class );
private final static String UTF8 = "UTF-8";
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private long mLastConnectionAttempt = 0;
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
private AtomicBoolean mConnecting = new AtomicBoolean();
private Socket mSocket;
private DataOutputStream mOutputStream;
private DataInputStream mInputStream;
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
private int mCounter;
private boolean mFirstMetadataUpdateSuppressed = false;
private byte[] mSilenceFrame;
/**
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
* compatible with Icecast version 2.3.2 and older versions of the server software.
*
* Note: use @see IcecastHTTPBroadcaster for Icecast version 2.4.x and newer.
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
*
* @param configuration
* @param audioConverter
*/
public IcecastTCPBroadcaster(ThreadPoolManager threadPoolManager,
IcecastTCPConfiguration configuration,
IAudioConverter audioConverter)
public IcecastTCPAudioBroadcaster(DefaultAsyncHttpClient httpClient,
ThreadPoolManager threadPoolManager,
IcecastTCPConfiguration configuration)
{
super(threadPoolManager, configuration, audioConverter);
super(threadPoolManager, configuration);
mDefaultAsyncHttpClient = httpClient;
}
/**
* Configuration information
*/
private IcecastHTTPConfiguration getConfiguration()
private IcecastTCPConfiguration getConfiguration()
{
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
return (IcecastTCPConfiguration)getBroadcastConfiguration();
}
/**
* Broadcast any queued audio packets using Icecast V2 Protocol
*/
@Override
public void broadcast()
protected void broadcastAudio(byte[] audio)
{
if(connect())
boolean completed = false;
while(!completed)
{
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
if (!mPacketsToBroadcast.isEmpty())
if(connect())
{
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mLog.debug("We have [" + mPacketsToBroadcast.size() +
"] packets - sending:" + convertedAudio.length + " bytes");
try
{
send(convertedAudio);
send(audio);
completed = true;
}
catch(SocketException se)
{
disconnect();
//The remote server likely disconnected - setup to reconnect
mLog.error("Socket Exception on Icecast TCP Audio Broadcaster -- disconnecting and resetting", se);
disconnect(false);
}
catch(IOException e)
{
completed = true;
mLog.error("Error sending audio", e);
}
}
else
{
completed = true;
}
}
}
mPacketsToBroadcast.clear();
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
if(connected() && mFirstMetadataUpdateSuppressed)
{
StringBuilder sb = new StringBuilder();
mCounter++;
if(metadata != null)
{
Metadata to = metadata.getMetadata(MetadataType.TO);
if(mCounter % 10 == 0)
sb.append("TO:");
if(to != null)
{
updateMetadata("Song Count " + mCounter);
if(to.hasAlias())
{
sb.append(to.getAlias().getName());
}
else
{
sb.append(to.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
Metadata from = metadata.getMetadata(MetadataType.FROM);
sb.append(" FROM:");
if(from != null)
{
if(from.hasAlias())
{
sb.append(from.getAlias().getName());
}
else
{
sb.append(from.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
}
else
{
sb.append("Scanning ....");
}
try
{
if(mDefaultAsyncHttpClient == null)
{
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
}
String songEncoded = URLEncoder.encode(sb.toString(), UTF8);
StringBuilder query = new StringBuilder();
query.append("/admin/metadata?mode=updinfo");
query.append("&mount=").append(getConfiguration().getMountPoint());
query.append("&charset=UTF%2d8");
query.append("&song=").append(songEncoded);
Uri uri = new Uri(Uri.HTTP, null, getConfiguration().getHost(), getConfiguration().getPort(),
query.toString(), null);
BoundRequestBuilder builder = mDefaultAsyncHttpClient.prepareGet(uri.toUrl());
//Use Basic (base64) authentication
Realm realm = new Realm.Builder(getConfiguration().getUserName(), getConfiguration().getPassword())
.setScheme(Realm.AuthScheme.BASIC).setUsePreemptiveAuth(true).build();
builder.setRealm(realm);
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
AsyncHandler handler = new AsyncCompletionHandler()
{
@Override
public Object onCompleted(Response response) throws Exception
{
return null;
}
};
mDefaultAsyncHttpClient.executeRequest(builder.build(), handler);
}
catch(Exception e)
{
mLog.debug("Error while updating metadata", e);
}
}
else if(connected())
{
mFirstMetadataUpdateSuppressed = true;
}
}
@@ -152,7 +256,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
/**
* Disconnect from the remote server
*/
public void disconnect()
public void disconnect(boolean shutdown)
{
if(mOutputStream != null)
{
@@ -197,7 +301,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
mSocket = null;
}
mLog.debug("Icecast stream is now disconnected");
if(shutdown)
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
else
{
setBroadcastState(BroadcastState.READY);
}
}
/**
@@ -226,7 +337,6 @@ public class IcecastTCPBroadcaster extends Broadcaster
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
@@ -334,6 +444,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
getConfiguration().getPort() + "] - will reattempt connection periodically");
return;
}
catch(ConnectException ce)
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Connection refused. Unable to create connection to streaming server host[" +
getConfiguration().getHost() + "] and port[" +
getConfiguration().getPort() + "]");
return;
}
catch(IOException ioe)
{
setBroadcastState(BroadcastState.ERROR);
@@ -457,7 +575,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if(response.startsWith("Invalid Password"))
else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
{
setBroadcastState(BroadcastState.INVALID_PASSWORD);
}
@@ -469,56 +587,20 @@ public class IcecastTCPBroadcaster extends Broadcaster
}
else
{
setBroadcastState(BroadcastState.CONNECTED);
mLog.debug("Response was empty");
}
}
private void updateMetadata(String songName)
{
if(songName != null && !songName.isEmpty())
{
try
{
if(mDefaultAsyncHttpClient == null)
{
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
}
String songEncoded = URLEncoder.encode(songName, UTF8);
StringBuilder query = new StringBuilder();
query.append("/admin/metadata?mode=updinfo");
query.append("&mount=").append(getConfiguration().getMountPoint());
query.append("&charset=UTF%2d8");
query.append("&song=").append(songEncoded);
Uri uri = new Uri(Uri.HTTP, null, getConfiguration().getHost(), getConfiguration().getPort(),
query.toString(), null);
BoundRequestBuilder builder = mDefaultAsyncHttpClient.prepareGet(uri.toUrl());
//Use Basic (base64) authentication
Realm realm = new Realm.Builder(getConfiguration().getUserName(), getConfiguration().getPassword())
.setScheme(Realm.AuthScheme.BASIC).setUsePreemptiveAuth(true).build();
builder.setRealm(realm);
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mDefaultAsyncHttpClient.executeRequest(builder.build());
}
catch(Exception e)
{
mLog.debug("Error while updating metadata", e);
}
}
}
public static void main(String[] args)
{
boolean test = false;
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
config.setName("Broadcastify SDRTrunk #2");
String streamName = "Broadcastify SDRTrunk #2";
config.setName(streamName);
config.setHost("audio3.broadcastify.com");
config.setPort(80);
config.setMountPoint("/k0yrdpx7zn4h");
@@ -530,6 +612,11 @@ public class IcecastTCPBroadcaster extends Broadcaster
config.setURL("http://www.radioreference.com");
config.setBitRate(16);
AudioMetadata metadata = new AudioMetadata(0, false);
Alias toAlias = new Alias("ToAlias");
toAlias.addAliasID(new BroadcastChannel(streamName));
metadata.receive(new Metadata(MetadataType.TO, "1234", toAlias ));
if(test)
{
mLog.debug("Auth:" + config.getEncodedCredentials());
@@ -537,22 +624,29 @@ public class IcecastTCPBroadcaster extends Broadcaster
else
{
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
mLog.debug("Registering and starting audio playback");
int playCount = 0;
while(true)
{
mLog.debug("Playback started [" + path.toString() + "]");
toAlias.setName("Playback #" + playCount++);
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.setMetadata(metadata);
reader.setListener(audioBroadcaster);
reader.read();
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
}
catch (IOException e)
{

View File

@@ -22,9 +22,10 @@ import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import java.util.Base64;
@@ -32,6 +33,8 @@ import java.util.Base64;
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
public class IcecastTCPConfiguration extends BroadcastConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
private String mUserName;
private String mMountPoint;
private String mDescription;
@@ -44,6 +47,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
public IcecastTCPConfiguration()
{
setUserName("source");
//No-arg constructor for JAXB
}
@@ -54,6 +58,31 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
public IcecastTCPConfiguration(BroadcastFormat format)
{
super(format);
setUserName("source");
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
return copy;
}
@Override
@@ -79,6 +108,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
sb.append(getPassword());
}
mLog.debug("Connection String: " + sb.toString());
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
return "Basic " + base64;
@@ -175,7 +206,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Public visibility of the broadcast
* Public visibility of the broadcastAudio
*/
@XmlAttribute( name="public" )
public boolean isPublic()
@@ -184,8 +215,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
@@ -193,7 +224,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Number of audio channels in the broadcast
* Number of audio channels in the broadcastAudio
*/
@XmlAttribute( name="channels" )
public int getChannels()
@@ -202,7 +233,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Sets the number of audio channels in the broadcast
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
@@ -254,7 +285,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
*/
@XmlAttribute( name="url" )
public String getURL()
@@ -263,7 +294,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)
@@ -275,4 +306,35 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
{
return mURL != null;
}
@Override
public boolean isValid()
{
if(!super.isValid())
{
return false;
}
else if(mUserName == null)
{
return false;
}
else if(mMountPoint == null)
{
return false;
}
else if(mChannels != 1)
{
return false;
}
else if(mSampleRate <= 0)
{
return false;
}
else if(mBitRate <= 0)
{
return false;
}
return true;
}
}

View File

@@ -19,13 +19,14 @@
package audio.broadcast.shoutcast.v1;
import audio.AudioPacket;
import audio.broadcast.Broadcaster;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastMetadata;
import audio.broadcast.BroadcastState;
import audio.convert.IAudioConverter;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.wave.AudioPacketMonoWaveReader;
@@ -41,9 +42,9 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class ShoutcastV1Broadcaster extends Broadcaster
public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1Broadcaster.class );
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private long mLastConnectionAttempt = 0;
@@ -58,13 +59,10 @@ public class ShoutcastV1Broadcaster extends Broadcaster
/**
* Creates a Shoutcast Version 1 broadcaster.
* @param configuration
* @param audioConverter
*/
public ShoutcastV1Broadcaster(ThreadPoolManager threadPoolManager,
ShoutcastV1Configuration configuration,
IAudioConverter audioConverter)
public ShoutcastV1AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration configuration)
{
super(threadPoolManager, configuration, audioConverter);
super(threadPoolManager, configuration);
}
/**
@@ -75,34 +73,29 @@ public class ShoutcastV1Broadcaster extends Broadcaster
return (ShoutcastV1Configuration)getBroadcastConfiguration();
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to send audio metadata to shoutcast v1 server - needs code");
}
/**
* Process audio packets using Shoutcast V1 Protocol
*/
public void broadcast()
@Override
protected void broadcastAudio(byte[] audio)
{
//If we're connected, send the audio, otherwise discard it
if(connect())
{
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
if(!mPacketsToBroadcast.isEmpty())
try
{
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mPacketsToBroadcast.clear();
if(convertedAudio != null)
{
try
{
send(convertedAudio);
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
}
}
send(audio);
}
catch (IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
}
}
}
@@ -349,8 +342,9 @@ public class ShoutcastV1Broadcaster extends Broadcaster
config.setBitRate(16);
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
@@ -363,7 +357,7 @@ public class ShoutcastV1Broadcaster extends Broadcaster
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.setListener(audioBroadcaster);
reader.read();
}
catch (IOException e)

View File

@@ -21,6 +21,7 @@ package audio.broadcast.shoutcast.v1;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.icecast.IcecastTCPConfiguration;
public class ShoutcastV1Configuration extends BroadcastConfiguration
{
@@ -31,11 +32,40 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
private int mBitRate;
private String mURL;
public ShoutcastV1Configuration()
{
//No-arg JAXB constructor
}
public ShoutcastV1Configuration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
ShoutcastV1Configuration copy = new ShoutcastV1Configuration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setStreamName(getStreamName());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setChannels(getChannels());
copy.setBitRate(getBitRate());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
@@ -77,7 +107,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
}
/**
* Public visibility of the broadcast
* Public visibility of the broadcastAudio
*/
public boolean isPublic()
{
@@ -85,8 +115,8 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
@@ -94,7 +124,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
}
/**
* Number of audio channels in the broadcast
* Number of audio channels in the broadcastAudio
*/
public int getChannels()
{
@@ -102,7 +132,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
}
/**
* Sets the number of audio channels in the broadcast
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
@@ -127,7 +157,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
*/
public String getURL()
{
@@ -135,7 +165,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)

View File

@@ -19,10 +19,10 @@
package audio.broadcast.shoutcast.v2;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre;
@@ -39,8 +39,9 @@ import audio.broadcast.shoutcast.v2.message.UltravoxMessageFactory;
import audio.broadcast.shoutcast.v2.message.UltravoxMessageType;
import audio.broadcast.shoutcast.v2.message.UltravoxMetadata;
import audio.broadcast.shoutcast.v2.message.XMLMetadata;
import audio.convert.IAudioConverter;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
@@ -57,9 +58,9 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class ShoutcastV2Broadcaster extends Broadcaster
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2Broadcaster.class );
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private static final boolean GET_RESPONSE = true;
@@ -75,13 +76,10 @@ public class ShoutcastV2Broadcaster extends Broadcaster
/**
* Creates a Shoutcast Version 2 broadcaster.
* @param configuration details for shoutcast version 2
* @param audioConverter to convert PCM audio
*/
public ShoutcastV2Broadcaster(ThreadPoolManager threadPoolManager,
BroadcastConfiguration configuration,
IAudioConverter audioConverter)
public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration configuration)
{
super(threadPoolManager, configuration, audioConverter);
super(threadPoolManager, configuration);
}
/**
@@ -92,41 +90,36 @@ public class ShoutcastV2Broadcaster extends Broadcaster
return (ShoutcastV2Configuration)getBroadcastConfiguration();
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to broadcast audio metadata to shoutcast v2 server - needs coding");
}
/**
* Broadcast audio packets
* Broadcast audio
*/
public void broadcast()
@Override
protected void broadcastAudio(byte[] audio)
{
//If we're connected, send the audio, otherwise discard it
if(connect())
{
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
List<UltravoxMessage> audioMessages = getAudioMessages(audio);
if(!mPacketsToBroadcast.isEmpty())
try
{
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mPacketsToBroadcast.clear();
if(convertedAudio != null)
for(UltravoxMessage audioMessage: audioMessages)
{
List<UltravoxMessage> audioMessages = getAudioMessages(convertedAudio);
try
{
for(UltravoxMessage audioMessage: audioMessages)
{
send(audioMessage, NO_RESPONSE_EXPECTED);
}
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
return;
}
send(audioMessage, NO_RESPONSE_EXPECTED);
}
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
return;
}
}
}
@@ -370,13 +363,13 @@ public class ShoutcastV2Broadcaster extends Broadcaster
response = send(icyName, NO_RESPONSE_EXPECTED);
}
if (getShoutcastConfiguration().getStreamGenre() != null)
if (getShoutcastConfiguration().getGenre() != null)
{
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getStreamGenre()));
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getGenre()));
ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
icyGenre.setGenre(getShoutcastConfiguration().getStreamGenre());
icyGenre.setGenre(getShoutcastConfiguration().getGenre());
response = send(icyGenre, NO_RESPONSE_EXPECTED);
}
@@ -534,7 +527,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
config.setPort(8000);
config.setPassword("denny3");
config.setStreamName("Denny's Audio Broadcast Test");
config.setStreamGenre("Public Safety");
config.setGenre("Public Safety");
config.setPublic(true);
config.setURL("http://localhost:8000");
config.setBitRate(16);
@@ -545,8 +538,9 @@ public class ShoutcastV2Broadcaster extends Broadcaster
else
{
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
@@ -559,7 +553,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.setListener(audioBroadcaster);
reader.read();
}
catch (IOException e)

View File

@@ -28,16 +28,46 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
private String mUserID;
private int mBitRate;
private String mStreamName;
private String mStreamGenre;
private String mGenre;
private String mURL;
private boolean mPublic = true;
public ShoutcastV2Configuration()
{
//No-arg JAXB constructor
}
public ShoutcastV2Configuration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
ShoutcastV2Configuration copy = new ShoutcastV2Configuration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Shoutcast V2 Configuration Parameters
copy.setStreamID(getStreamID());
copy.setUserID(getUserID());
copy.setBitRate(getBitRate());
copy.setStreamName(getStreamName());
copy.setGenre(getGenre());
copy.setURL(getURL());
copy.setPublic(isPublic());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
@@ -117,21 +147,21 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
/**
* Stream genre
*/
public String getStreamGenre()
public String getGenre()
{
return mStreamGenre;
return mGenre;
}
/**
* Sets the genre tag for this stream
*/
public void setStreamGenre(String genre)
public void setGenre(String genre)
{
mStreamGenre = genre;
mGenre = genre;
}
/**
* Public visibility of the broadcast
* Public visibility of the broadcastAudio
*/
public boolean isPublic()
{
@@ -139,8 +169,8 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
@@ -148,7 +178,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
*/
public String getURL()
{
@@ -156,7 +186,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)

View File

@@ -45,7 +45,7 @@ public class StreamMimeType extends UltravoxMessage
}
/**
* Sets the broadcast format or MIME type
* Sets the broadcastAudio format or MIME type
* @param format of the stream
*/
public void setFormat(BroadcastFormat format)

View File

@@ -21,7 +21,7 @@ package audio.broadcast.shoutcast.v2.message;
public class TerminateBroadcast extends UltravoxMessage
{
/**
* Client request to server to terminate the broadcast
* Client request to server to terminate the broadcastAudio
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/

View File

@@ -18,11 +18,15 @@
package audio.metadata;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import alias.id.broadcast.BroadcastChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -312,6 +316,23 @@ public class AudioMetadata implements Listener<Metadata>
return mStreamable;
}
public Collection<BroadcastChannel> getBroadcastChannels()
{
Set<BroadcastChannel> broadcastChannels = new TreeSet<>();
for(Metadata metadata: mMetadata.values())
{
if(metadata.hasAlias() && metadata.getAlias().isStreamable())
{
Alias alias = metadata.getAlias();
broadcastChannels.addAll(alias.getBroadcastChannels());
}
}
return broadcastChannels;
}
public String toString()
{
StringBuilder sb = new StringBuilder();

View File

@@ -91,7 +91,7 @@ public class ControllerPanel extends JPanel
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
mBroadcastPanel = new BroadcastPanel(broadcastModel);
mBroadcastPanel = new BroadcastPanel(broadcastModel, settingsManager);
mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);

View File

@@ -20,7 +20,6 @@ package gui;
import alias.AliasModel;
import alias.action.AliasActionManager;
import audio.AudioManager;
import audio.broadcast.AudioBroadcastManager;
import audio.broadcast.BroadcastModel;
import com.jidesoft.swing.JideSplitPane;
import controller.ControllerPanel;
@@ -137,10 +136,9 @@ public class SDRTrunk implements Listener<TunerEvent>
AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager());
channelProcessingManager.addAudioPacketListener(audioManager);
BroadcastModel broadcastModel = new BroadcastModel();
BroadcastModel broadcastModel = new BroadcastModel(threadPoolManager, mSettingsManager);
AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager);
channelProcessingManager.addAudioPacketListener(audioBroadcastManager);
channelProcessingManager.addAudioPacketListener(broadcastModel);
MapService mapService = new MapService(mSettingsManager);
channelProcessingManager.addMessageListener(mapService);

View File

@@ -31,13 +31,13 @@ public class DecoderSelectionFrame extends JInternalFrame
public DecoderSelectionFrame( JDesktopPane desktop,
IControllableFileSource source )
{
ThreadPoolManager tpm = new ThreadPoolManager();
AliasModel aliasModel = new AliasModel();
BroadcastModel broadcastModel = new BroadcastModel();
BroadcastModel broadcastModel = new BroadcastModel(tpm, null);
ChannelModel channelModel = new ChannelModel();
ChannelMapModel channelMapModel = new ChannelMapModel();
ThreadPoolManager tpm = new ThreadPoolManager();
mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel );
mDesktop = desktop;

View File

@@ -33,7 +33,7 @@ import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import audio.broadcast.BroadcastConfigurationEvent;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -109,10 +109,10 @@ public class PlaylistManager implements ChannelEventListener
}
} );
mBroadcastModel.addListener(new Listener<BroadcastConfigurationEvent>()
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastConfigurationEvent broadcastConfigurationEvent)
public void receive(BroadcastEvent broadcastEvent)
{
schedulePlaylistSave();
}
@@ -346,7 +346,7 @@ public class PlaylistManager implements ChannelEventListener
}
catch ( Exception e )
{
mLog.error( "coulcn't open inputstream to load playlist [" + playlistPath.toString() + "]", e );
mLog.error( "couldn't open input stream to load playlist [" + playlistPath.toString() + "]", e );
}
finally
{

View File

@@ -55,6 +55,8 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
private BufferProcessor mBufferProcessor;
private ScheduledFuture<?> mProcessorHandle;
private long mSampleCount;
/**
* Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file
*
@@ -95,6 +97,15 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
return mTimeRecordingStart;
}
/**
* Recording length in milliseconds
*/
public long getRecordingLength()
{
//Assumes audio sample rate of 8000 samples/second or 8 samples/milli-second
return mSampleCount / 8;
}
/**
* Implements the IAudioPacketListener interface and simply redirects to the Listener<AudioPacket> interface.
* This is necessary since you can't have multiple methods with the same erasure (ie Listener<xxx>) in the
@@ -137,7 +148,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
*/
protected OutputStream getOutputStream()
{
return null;
return mFileOutputStream;
}
/**
@@ -169,6 +180,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
{
try
{
mFileOutputStream.flush();
mFileOutputStream.close();
}
catch (IOException e)
@@ -204,8 +216,6 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
{
mFileOutputStream = new FileOutputStream(mPath.toFile());
mLog.info("Created Audio Recording [" + getPath().toString() + "]");
/* Schedule the handler to run every half second */
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
}
@@ -230,6 +240,14 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
try
{
record(mPacketsToProcess);
for(AudioPacket packet: mPacketsToProcess)
{
if(packet.getType() == AudioPacket.Type.AUDIO)
{
mSampleCount += packet.getAudioBuffer().getSamples().length;
}
}
}
catch(IOException ioe)
{

View File

@@ -95,26 +95,50 @@ public class MP3Recorder extends AudioRecorder
public static void main(String[] args)
{
Path inputPath = Paths.get("/home/denny/Music/PCM.wav");
Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
mLog.debug("Starting ...");
// Path inputPath = Paths.get("/home/denny/Music/PCM.wav");
// Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
//
// mLog.debug("Reading: " + inputPath.toString());
// mLog.debug("Writing: " + outputPath.toString());
//
// final MP3Recorder recorder = new MP3Recorder(outputPath);
// recorder.start(Executors.newSingleThreadScheduledExecutor());
//
// try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(inputPath, true))
// {
// reader.setListener(recorder);
// reader.read();
// recorder.stop();
// }
// catch (IOException e)
// {
// mLog.error("Error", e);
// }
//
Path outputPath = Paths.get("/home/denny/Music/Silence_test_10_seconds.mp3");
mLog.debug("Reading: " + inputPath.toString());
mLog.debug("Writing: " + outputPath.toString());
final MP3Recorder recorder = new MP3Recorder(outputPath);
recorder.start(Executors.newSingleThreadScheduledExecutor());
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
recorder.start(executor);
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(inputPath, true))
long duration = 1165;
int length = (int)(duration * 8); //8000 Hz sample rate
float[] silence = new float[length];
AudioPacket silencePacket = new AudioPacket(silence, null);
for(int x = 0; x < 10; x++)
{
reader.setListener(recorder);
reader.read();
recorder.stop();
}
catch (IOException e)
{
mLog.error("Error", e);
recorder.receive(silencePacket);
}
recorder.stop();
executor.shutdown();
mLog.debug("Finished");
}
}

View File

@@ -21,7 +21,7 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Broadcasts a received float sample to multiple listeners
* Broadcasts an item to multiple listeners
*/
public class Broadcaster<T> implements Listener<T>
{