/* * ***************************************************************************** * Copyright (C) 2014-2020 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 io.github.dsheirer.audio.broadcast; import io.github.dsheirer.audio.AudioSegment; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.record.AudioSegmentRecorder; import io.github.dsheirer.record.RecordFormat; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.TimeStamp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on * disk and enqueues the temporary recording for streaming. */ public class AudioStreamingManager implements Listener { private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingManager.class); private LinkedTransferQueue mNewAudioSegments = new LinkedTransferQueue<>(); private List mAudioSegments = new ArrayList<>(); private Listener mAudioRecordingListener; private BroadcastFormat mBroadcastFormat; private UserPreferences mUserPreferences; private ScheduledFuture mAudioSegmentProcessorFuture; private int mNextRecordingNumber = 1; /** * Constructs an instance * @param listener to receive completed audio recordings * @param broadcastFormat for temporary recordings * @param userPreferences to manage recording directories */ public AudioStreamingManager(Listener listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences) { mAudioRecordingListener = listener; mBroadcastFormat = broadcastFormat; mUserPreferences = userPreferences; } /** * Primary receive method */ @Override public void receive(AudioSegment audioSegment) { mNewAudioSegments.add(audioSegment); } /** * Starts the scheduled audio segment processor */ public void start() { if(mAudioSegmentProcessorFuture == null) { mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(), 0, 250, TimeUnit.MILLISECONDS); } } /** * Stops the scheduled audio segment processor */ public void stop() { if(mAudioSegmentProcessorFuture != null) { mAudioSegmentProcessorFuture.cancel(true); mAudioSegmentProcessorFuture = null; } for(AudioSegment audioSegment: mNewAudioSegments) { audioSegment.decrementConsumerCount(); } mNewAudioSegments.clear(); for(AudioSegment audioSegment: mAudioSegments) { audioSegment.decrementConsumerCount(); } mAudioSegments.clear(); } /** * Main processing method to process audio segments */ private void processAudioSegments() { mNewAudioSegments.drainTo(mAudioSegments); Iterator it = mAudioSegments.iterator(); AudioSegment audioSegment; while(it.hasNext()) { audioSegment = it.next(); if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled()) { it.remove(); audioSegment.decrementConsumerCount(); } else if(audioSegment.completeProperty().get()) { it.remove(); if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels()) { Path path = getTemporaryRecordingPath(); long length = 0; for(float[] audioBuffer: audioSegment.getAudioBuffers()) { length += audioBuffer.length; } length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond. try { AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3); IdentifierCollection identifierCollectionCopy = new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers()); AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(), identifierCollectionCopy, audioSegment.getStartTimestamp(), length); mAudioRecordingListener.receive(audioRecording); } catch(IOException ioe) { mLog.error("Error recording temporary stream MP3"); } } audioSegment.decrementConsumerCount(); } } } /** * Creates a temporary streaming recording file path */ private Path getTemporaryRecordingPath() { StringBuilder sb = new StringBuilder(); sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX); //Check for integer overflow and readjust negative value to 0 if(mNextRecordingNumber < 0) { mNextRecordingNumber = 1; } int recordingNumber = mNextRecordingNumber++; sb.append(recordingNumber).append("_"); sb.append(TimeStamp.getLongTimeStamp("_")); sb.append(mBroadcastFormat.getFileExtension()); Path temporaryRecordingPath = mUserPreferences.getDirectoryPreference().getDirectoryStreaming().resolve(sb.toString()); return temporaryRecordingPath; } /** * Scheduled runnable to process audio segments. */ public class AudioSegmentProcessor implements Runnable { @Override public void run() { try { processAudioSegments(); } catch(Throwable t) { mLog.error("Error processing audio segments for streaming", t); } } } }