/******************************************************************************* * 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 record; import audio.AudioPacket; import audio.IAudioPacketListener; import channel.metadata.Metadata; import module.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public abstract class AudioRecorder extends Module implements Listener<AudioPacket>, IAudioPacketListener { private final static Logger mLog = LoggerFactory.getLogger(AudioRecorder.class); private LinkedBlockingQueue<AudioPacket> mAudioPacketQueue = new LinkedBlockingQueue<>(500); private List<AudioPacket> mPacketsToProcess = new ArrayList<>(); private FileOutputStream mFileOutputStream; private AtomicBoolean mRunning = new AtomicBoolean(); protected Path mPath; protected Metadata mMetadata; protected long mTimeRecordingStart; protected long mTimeLastPacketReceived; private BufferProcessor mBufferProcessor; private ScheduledFuture<?> mProcessorHandle; private Listener<AudioRecorder> mRecordingClosedListener; private long mSampleCount; /** * Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file * * @param path for the output recording */ public AudioRecorder(Path path) { mPath = path; } /** * Path for the audio recording file */ public Path getPath() { return mPath; } /** * Latest audio metadata received for this recording */ public Metadata getMetadata() { return mMetadata; } /** * Timestamp of the last buffer received by this recorder - allows this recorder to be monitored for automatic * closure after a time period has elapsed. */ public long getTimeLastPacketReceived() { return mTimeLastPacketReceived; } public long getTimeRecordingStart() { 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 * parent module class. */ @Override public Listener<AudioPacket> getAudioPacketListener() { return this; } /** * Processes the audio packet and captures the latest Metadata for the recording for easy access. */ @Override public void receive(AudioPacket audioPacket) { if(mRunning.get()) { mTimeRecordingStart = System.currentTimeMillis(); mTimeLastPacketReceived = mTimeRecordingStart; if(audioPacket.hasMetadata()) { mMetadata = audioPacket.getMetadata(); } boolean success = mAudioPacketQueue.offer(audioPacket); if(!success) { mLog.error("recorder buffer overflow - stopping recorder [" + getPath().toString() + "]"); stop(); } } } /** * File output stream for the current recording. Intended to allow sub-classes to write binary data to the file. */ protected OutputStream getOutputStream() { return mFileOutputStream; } /** * Stops the recorder and flags the recording to be closed. Use this method if you do not need any details about * the final recording. Otherwise, use the close() method and register a closing listener. */ public void stop() { close(null); } /** * Closes the recording file. Upon successful closing of the recording file, the listener is notified that the * audio recorder is closed. There is potential for the calling thread (here) and the buffer processor thread to * both inform the recording closed listener that the recording is ended. So, we synchronize on the listener and * the first thread to get the lock informs the listener and then nullifies the listener pointer so that if the * second thread attempts a duplicate notification the listener would be null at that point. */ public void close(Listener<AudioRecorder> listener) { mRecordingClosedListener = listener; if(!mRunning.compareAndSet(true, false)) { synchronized(mRecordingClosedListener) { if(mRecordingClosedListener != null) { mRecordingClosedListener.receive(AudioRecorder.this); mRecordingClosedListener = null; } } } } /** * Records the list of audio packets in the sub-class specific audio format. */ protected abstract void record(List<AudioPacket> audioPackets) throws IOException; /** * Starts this recorder as a scheduled thread running under the executor argument * * @param executor to use in scheduling audio conversion and file writes. */ public void start(ScheduledExecutorService executor) { if(mRunning.compareAndSet(false, true)) { mTimeLastPacketReceived = System.currentTimeMillis(); if(mBufferProcessor == null) { mBufferProcessor = new BufferProcessor(); } try { mFileOutputStream = new FileOutputStream(mPath.toFile()); /* Schedule the handler to run every half second */ mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); } catch(IOException io) { mLog.error("Error starting audio recorder [" + getPath().toString() + "]", io); mRunning.set(false); } } } /** * Processes the audio packet queue. */ private void processAudioPacketQueue() { mAudioPacketQueue.drainTo(mPacketsToProcess); if(!mPacketsToProcess.isEmpty()) { try { record(mPacketsToProcess); for(AudioPacket packet : mPacketsToProcess) { if(packet.getType() == AudioPacket.Type.AUDIO) { mSampleCount += packet.getAudioBuffer().getSamples().length; } } } catch(IOException ioe) { mLog.debug("Error while recording audio to [" + getPath().toString() + "] - stopping recorder"); stop(); } mPacketsToProcess.clear(); } } /** * Disposes this audio recorder and prepares it for reclamation */ @Override public void dispose() { stop(); } /** * Not implemented. Recorder modules are not appropriate for reset and reuse. */ @Override public void reset() { } /** * Flushes any remaining audio to the output file. This method should be implemented by subclasses where an audio * converter may contain residual frame data that should be flushed to disk before closing the audio file. */ protected void flush() { } /** * Drains the audio packet queue and records the audio packets to file */ public class BufferProcessor implements Runnable { private AtomicBoolean mProcessing = new AtomicBoolean(); public void run() { if(mProcessing.compareAndSet(false, true)) { processAudioPacketQueue(); //If we've been stopped or closed, finish the queue, close the recording, notify the listener, and // cancel the future if(!mRunning.get()) { //Allow sub-classes to flush remaining audio frame data to disk. flush(); if(mFileOutputStream != null) { try { mFileOutputStream.flush(); mFileOutputStream.close(); } catch(IOException e) { mLog.error("Error closing output stream", e); } } synchronized(mRecordingClosedListener) { if(mRecordingClosedListener != null) { mRecordingClosedListener.receive(AudioRecorder.this); mRecordingClosedListener = null; } } if(mProcessorHandle != null) { mProcessorHandle.cancel(false); mProcessorHandle = null; } } mProcessing.set(false); } } } }