/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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.source.wave;
import io.github.dsheirer.sample.ConversionUtils;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
import io.github.dsheirer.source.ComplexSource;
import io.github.dsheirer.source.IControllableFileSource;
import io.github.dsheirer.source.IFrameLocationListener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.ThreadPool;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class ComplexWaveSource extends ComplexSource implements IControllableFileSource, AutoCloseable
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexWaveSource.class);
private IFrameLocationListener mFrameLocationListener;
private int mBytesPerFrame;
private int mFrameCounter = 0;
private long mFrequency = 0;
private Listener mListener;
private AudioInputStream mInputStream;
private File mFile;
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("ComplexWaveSource");
private boolean mAutoReplay;
private ScheduledFuture> mReplayController;
/**
* Constructs an instance with optional auto-replay at near real time.
* @param file containing complex I/Q sample data
* @param autoReplay to enable continuous looping, real-time playback of sample data
*/
public ComplexWaveSource(File file, boolean autoReplay) throws IOException
{
if(file == null || !file.exists() || !supports(file))
{
throw new IOException("Empty or null file");
}
if(!supports(file))
{
throw new IOException("Unsupported file format");
}
mFile = file;
mAutoReplay = autoReplay;
}
public ComplexWaveSource(File file) throws IOException
{
this(file, false);
}
@Override
public void setSourceEventListener(Listener listener)
{
//Not implemented
}
@Override
public void removeSourceEventListener()
{
//Not implemented
}
@Override
public Listener getSourceEventListener()
{
//Not implemented
return null;
}
@Override
public void reset()
{
stop();
mFrameCounter = 0;
start();
}
/**
* Number of samples per buffer
*/
public int getBufferSampleCount()
{
return (int)(getSampleRate() / 20.0d);
}
@Override
public void start()
{
if(mInputStream == null)
{
try
{
open();
}
catch(Exception e)
{
mLog.error("Error", e);
}
}
if(mAutoReplay)
{
long intervalMilliseconds = 50; //20 intervals per second
double framesPerInterval = getSampleRate() / 20.0d;
mReplayController = ThreadPool.SCHEDULED.scheduleAtFixedRate(new ReplayController(framesPerInterval),
0, intervalMilliseconds, TimeUnit.MILLISECONDS);
}
}
@Override
public void stop()
{
try
{
if(mReplayController != null)
{
mReplayController.cancel(true);
}
close();
}
catch(IOException e)
{
mLog.error("Error stopping complex wave source");
}
}
@Override
public long getFrameCount() throws IOException
{
// TODO Auto-generated method stub
return 0;
}
@Override
public double getSampleRate()
{
if(mInputStream != null)
{
return mInputStream.getFormat().getSampleRate();
}
return 0;
}
/**
* Returns the frequency set for this file. Normally returns zero, but
* the value can be set with setFrequency() method.
*/
public long getFrequency()
{
return mFrequency;
}
/**
* Changes the value returned from getFrequency() for this source.
*/
public void setFrequency(long frequency)
{
mFrequency = frequency;
}
/**
* Closes the source file
*/
public void close() throws IOException
{
if(mInputStream != null)
{
mInputStream.close();
mInputStream = null;
}
}
/**
* Opens the source file for reading
*/
public void open() throws IOException, UnsupportedAudioFileException
{
if(mInputStream == null)
{
mInputStream = AudioSystem.getAudioInputStream(mFile);
AudioFormat format = mInputStream.getFormat();
mBytesPerFrame = format.getFrameSize();
if(format.getChannels() != 2 || format.getSampleSizeInBits() != 16)
{
throw new IOException("Unsupported Wave Format - EXPECTED: 2 " +
"channels 16-bit samples FOUND: " +
mInputStream.getFormat().getChannels() + " channels " +
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples");
}
/* Broadcast that we're at frame location 0 */
broadcast(0);
}
}
/**
* Reads the number of frames and sends a buffer to the listener
*/
@Override
public void next(int frames) throws IOException
{
next(frames, true);
}
/**
* Reads the number of frames and optionally sends the buffer(s) to the listener
*/
public void next(int frames, boolean broadcast) throws IOException
{
if(mInputStream != null)
{
byte[] buffer = new byte[mBytesPerFrame * frames];
/* Fill the buffer with samples from the file */
int samplesRead = mInputStream.read(buffer);
mFrameCounter += samplesRead;
broadcast(mFrameCounter);
if(broadcast && mListener != null)
{
if(samplesRead < 0)
{
throw new IOException("End of file reached");
}
if(samplesRead < buffer.length)
{
buffer = Arrays.copyOf(buffer, samplesRead);
}
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
ReusableComplexBuffer reusableBuffer = mReusableComplexBufferQueue.getBuffer(samples.length);
System.arraycopy(samples, 0, reusableBuffer.getSamples(), 0, samples.length);
reusableBuffer.setTimestamp(System.currentTimeMillis());
mListener.receive(reusableBuffer);
}
}
}
/**
* Registers the listener to receive sample buffers as they are read from
* the wave file
*/
@Override
public void setListener(Listener listener)
{
mListener = listener;
}
/**
* Unregisters the listener from receiving sample buffers
*/
public void removeListener(Listener listener)
{
mListener = null;
}
@Override
public File getFile()
{
return mFile;
}
private void broadcast(int byteLocation)
{
int frameLocation = (int)(byteLocation / mBytesPerFrame);
if(mFrameLocationListener != null)
{
mFrameLocationListener.frameLocationUpdated(frameLocation);
}
}
@Override
public void setListener(IFrameLocationListener listener)
{
mFrameLocationListener = listener;
}
@Override
public void removeListener(IFrameLocationListener listener)
{
mFrameLocationListener = null;
}
/**
* Indicates if the file is a supported audio file type
*/
public static boolean supports(File file)
{
try(AudioInputStream ais = AudioSystem.getAudioInputStream(file))
{
AudioFormat format = ais.getFormat();
if(format.getChannels() == 2 && format.getSampleSizeInBits() == 16)
{
return true;
}
}
catch(Exception e)
{
//Do nothing, we'll return a default of false
}
return false;
}
public class ReplayController implements Runnable
{
private double mFramesPerInterval;
private int mFramesRead;
private int mIntervals;
public ReplayController(double framesPerInterval)
{
mFramesPerInterval = framesPerInterval;
}
@Override
public void run()
{
mIntervals++;
int framesToRead = (int) FastMath.floor((mIntervals * mFramesPerInterval) - mFramesRead);
try
{
next(framesToRead, true);
mFramesRead += framesToRead;
}
catch(IOException ioe)
{
mLog.debug("End of Recording - looping [" + ioe.getLocalizedMessage() + "]");
reset();
}
}
}
}