diff --git a/config/logback.xml b/config/logback.xml index 42146503..9fe5f368 100644 --- a/config/logback.xml +++ b/config/logback.xml @@ -27,13 +27,7 @@ - - - - + - - - \ No newline at end of file diff --git a/src/audio/broadcast/AudioBroadcaster.java b/src/audio/broadcast/AudioBroadcaster.java index 8688b04f..2fd41747 100644 --- a/src/audio/broadcast/AudioBroadcaster.java +++ b/src/audio/broadcast/AudioBroadcaster.java @@ -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 mAudioRecordingQueue = new LinkedTransferQueue<>(); - private byte[] mSilenceFrame; + private ISilenceGenerator mSilenceGenerator; private Listener 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); } diff --git a/src/audio/broadcast/BroadcastFactory.java b/src/audio/broadcast/BroadcastFactory.java index b9bf1226..2677e9cd 100644 --- a/src/audio/broadcast/BroadcastFactory.java +++ b/src/audio/broadcast/BroadcastFactory.java @@ -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 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 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"); } } } diff --git a/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java index 4a06907e..d40f9e32 100644 --- a/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java +++ b/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java @@ -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); } } diff --git a/src/audio/broadcast/icecast/IcecastMetadataUpdater.java b/src/audio/broadcast/icecast/IcecastMetadataUpdater.java index c290fb33..002a2790 100644 --- a/src/audio/broadcast/icecast/IcecastMetadataUpdater.java +++ b/src/audio/broadcast/icecast/IcecastMetadataUpdater.java @@ -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 diff --git a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java index c94cf7b9..c42cc11d 100644 --- a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java +++ b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java @@ -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); } } diff --git a/src/audio/convert/ISilenceGenerator.java b/src/audio/convert/ISilenceGenerator.java new file mode 100644 index 00000000..ac2e1c5c --- /dev/null +++ b/src/audio/convert/ISilenceGenerator.java @@ -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 + * + ******************************************************************************/ +package audio.convert; + +public interface ISilenceGenerator +{ + byte[] generate(long duration); +} diff --git a/src/audio/convert/MP3SilenceGenerator.java b/src/audio/convert/MP3SilenceGenerator.java new file mode 100644 index 00000000..7b088506 --- /dev/null +++ b/src/audio/convert/MP3SilenceGenerator.java @@ -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 + * + ******************************************************************************/ +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 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"); + } +}