#30 - improves silence frame handling. Appends silence to end of MP3 recordings to round them out to whole second intervals and adds an MP3 silence generator to generate 1 second silence frames each interval when there is no audio to broadcast. Previous approach used static silence frame, but we can't generate an exactly 1 second frame, so the remote server was resetting the connection when it detected that we were sending data at over or under rate.

This commit is contained in:
Denny
2016-12-26 11:52:25 -05:00
parent 524a76e583
commit 5611f594c7
8 changed files with 148 additions and 48 deletions

View File

@@ -27,13 +27,7 @@
<appender-ref ref="STDOUT" />
</root>
<!-- Async HTTP Client - set level to info to filter debug messages -->
<logger name="org.asynchttpclient.netty" level="INFO" />
<!-- Async HTTP Client - set level to info to filter debug messages -->
<!-- Apache Mina TCP/HTTP library -->
<logger name="org.apache.mina" level="INFO" />
<!-- IO Netty - set level to info to filter debug messages -->
<logger name="io.netty" level="INFO" />
</configuration>

View File

@@ -20,6 +20,7 @@ package audio.broadcast;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.convert.ISilenceGenerator;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
@@ -47,7 +48,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
private LinkedTransferQueue<StreamableAudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private byte[] mSilenceFrame;
private ISilenceGenerator mSilenceGenerator;
private Listener<BroadcastEvent> mBroadcastEventListener;
private BroadcastState mBroadcastState = BroadcastState.READY;
@@ -85,8 +86,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
mBroadcastConfiguration = broadcastConfiguration;
mDelay = mBroadcastConfiguration.getDelay();
//Create a 1 second silence frame - from 1200 millis of silence
mSilenceFrame = BroadcastFactory.getSilenceFrame(getBroadcastConfiguration().getBroadcastFormat(), 1200);
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat());
mStreamManager = new StreamManager(threadPoolManager, this,
SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY));
@@ -326,7 +326,8 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private ByteArrayInputStream mInputStream;
private int mChunkSize;
private int mAverageBytesPerInterval;
private long mFinalSilencePadding = 0;
@Override
public void run()
@@ -335,12 +336,18 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{
if(mInputStream == null || mInputStream.available() <= 0)
{
if(mFinalSilencePadding > 0)
{
broadcastAudio(mSilenceGenerator.generate(mFinalSilencePadding));
mFinalSilencePadding = 0;
}
nextRecording();
}
if(mInputStream != null)
{
int length = Math.min(mChunkSize, mInputStream.available());
int length = Math.min(mAverageBytesPerInterval, mInputStream.available());
byte[] audio = new byte[length];
@@ -357,7 +364,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
}
else
{
broadcastAudio(mSilenceFrame);
broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS));
}
mProcessing.set(false);
@@ -395,17 +402,21 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{
mInputStream = new ByteArrayInputStream(audio);
int wholeIntervalChunks = (int)(recording.getRecordingLength() / PROCESSOR_RUN_INTERVAL_MS);
double intervals = (double)recording.getRecordingLength() / (double)PROCESSOR_RUN_INTERVAL_MS;
//Check for divide by zero situation
if(wholeIntervalChunks == 0)
mAverageBytesPerInterval = (int)((double)mInputStream.available() / intervals);
int wholeIntervals = (int)Math.floor(intervals) + 1;
//Silence padding to transmit at the end to keep stream length at multiples of run interval
mFinalSilencePadding = (wholeIntervals * PROCESSOR_RUN_INTERVAL_MS) -
recording.getRecordingLength();
if(connected())
{
wholeIntervalChunks = 1;
broadcastMetadata(recording.getAudioMetadata());
}
mChunkSize = (int)(mInputStream.available() / wholeIntervalChunks) + 1;
broadcastMetadata(recording.getAudioMetadata());
metadataUpdateRequired = false;
}
}
@@ -423,7 +434,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
}
//If we closed out a recording and don't have anything new, send an empty metadata update
if(metadataUpdateRequired)
if(metadataUpdateRequired && connected())
{
broadcastMetadata(null);
}

View File

@@ -33,7 +33,9 @@ import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
import audio.convert.IAudioConverter;
import audio.convert.ISilenceGenerator;
import audio.convert.MP3AudioConverter;
import audio.convert.MP3SilenceGenerator;
import controller.ThreadPoolManager;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
@@ -53,6 +55,20 @@ public class BroadcastFactory
public static final int MP3_MONO_16_KHZ_BITRATE = 16;
public static final boolean MP3_CONSTANT_BITRATE = false;
private static MP3AudioConverter MP3_SILENT_FRAME_GENERATOR;
static
{
MP3_SILENT_FRAME_GENERATOR = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
//We have to charge the converter pipeline with 1200 milliseconds to get 1 second of audio the first time
float[] silence = new float[9600]; //1200 milliseconds at 8000 kHz
AudioPacket silencePacket = new AudioPacket(silence, null);
List<AudioPacket> silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
MP3_SILENT_FRAME_GENERATOR.convert(silencePackets);
}
/**
* Creates an audio streaming broadcaster for the configuration
@@ -194,23 +210,15 @@ public class BroadcastFactory
}
}
public static byte[] getSilenceFrame(BroadcastFormat format, long duration)
public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
{
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)
switch (format)
{
case MP3:
MP3AudioConverter converter = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE,
MP3Recorder.CONSTANT_BIT_RATE);
return converter.convert(silencePackets);
return new MP3SilenceGenerator();
default:
throw new IllegalArgumentException("Unrecognized broadcast format: " + format.name());
throw new IllegalArgumentException("Unrecognized broadcast format [" + format +
"] can't create silence generator");
}
}
}

View File

@@ -80,16 +80,13 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
protected void broadcastAudio(byte[] audio)
{
if(connect())
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
if(mStreamingSession != null && mStreamingSession.isConnected())
{
IoBuffer buffer = IoBuffer.allocate(audio.length);
buffer.put(audio);
buffer.flip();
IoBuffer buffer = IoBuffer.allocate(audio.length);
buffer.put(audio);
buffer.flip();
mStreamingSession.write(buffer);
}
mStreamingSession.write(buffer);
}
}

View File

@@ -93,7 +93,8 @@ public class IcecastMetadataUpdater
{
//Single-use session - close it after we receive an error
session.closeNow();
mLog.error("Metadata update failed", cause);
mLog.error("Metadata update failed");
}
@Override

View File

@@ -73,12 +73,9 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
protected void broadcastAudio(byte[] audio)
{
if(connect())
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
if(mStreamingSession != null && mStreamingSession.isConnected())
{
mStreamingSession.write(audio);
}
mStreamingSession.write(audio);
}
}

View File

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

View File

@@ -0,0 +1,68 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.convert;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.convert.MP3AudioConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.mp3.MP3Recorder;
import java.util.ArrayList;
import java.util.List;
public class MP3SilenceGenerator implements ISilenceGenerator
{
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
/**
* Generates MP3 audio silence frames
*/
public MP3SilenceGenerator()
{
}
public byte[] generate(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);
return mGenerator.convert(silencePackets);
}
public static void main(String[] args)
{
mLog.debug("Starting ...");
MP3SilenceGenerator generator = new MP3SilenceGenerator();
for(long x = 0; x < 20; x ++)
{
byte[] silence = generator.generate(1000);
mLog.debug("Silence:" + x + " Length:" + silence.length);
}
mLog.debug("Finished");
}
}