mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#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:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
src/audio/convert/ISilenceGenerator.java
Normal file
24
src/audio/convert/ISilenceGenerator.java
Normal 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);
|
||||
}
|
||||
68
src/audio/convert/MP3SilenceGenerator.java
Normal file
68
src/audio/convert/MP3SilenceGenerator.java
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user