586 lines
18 KiB
Java
586 lines
18 KiB
Java
package util;
|
|
|
|
// Java imports
|
|
import java.util.concurrent.*;
|
|
import java.util.concurrent.locks.*;
|
|
import javax.sound.sampled.*;
|
|
|
|
// Background timer using either a realtime clock or audio for timing
|
|
public class FrameTimer {
|
|
|
|
// Instance fields
|
|
private Callback callback; // Event handler
|
|
private double rate; // Frame rate in seconds
|
|
private int source; // Timing source
|
|
private int state; // Operation state
|
|
|
|
// Audio fields
|
|
private int block; // Current byte buffer index
|
|
private byte[][] blocks; // Reusable byte buffers
|
|
private double buffer; // Buffer length in seconds
|
|
private int channels; // Number channels
|
|
private double delay; // Delay in seconds
|
|
private int frames; // Audio frames per timer frame
|
|
private SourceDataLine line; // Output line
|
|
private int sampleRate; // Rate in samples per second
|
|
private boolean written; // Samples have been written this frame
|
|
private LinkedBlockingQueue<byte[]> queue; // Sample queue
|
|
|
|
// Threading fields
|
|
private Thread audio; // Background audio processing
|
|
private CyclicBarrier cycAudio; // Synchronization with audio thread
|
|
private CyclicBarrier cycTimer; // Synchronization with timer thread
|
|
private ReentrantLock lock; // Allows callback to control the timer
|
|
private Thread timer; // Background timer processing
|
|
|
|
// Type for timer event handler
|
|
public interface Callback {
|
|
void onFrame(FrameTimer source);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Constants //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Sources
|
|
public static final int CLOCK = 0;
|
|
public static final int AUDIO = 1;
|
|
|
|
// States
|
|
public static final int STOPPED = 0;
|
|
public static final int RUNNING = 1;
|
|
public static final int PAUSED = 2;
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Constructors //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Default constructor
|
|
public FrameTimer() {
|
|
buffer = 0.05;
|
|
cycAudio = new CyclicBarrier(2);
|
|
cycTimer = new CyclicBarrier(2);
|
|
channels = 2;
|
|
delay = 0.1;
|
|
lock = new ReentrantLock();
|
|
queue = new LinkedBlockingQueue<byte[]>();
|
|
rate = 1;
|
|
sampleRate = 44100;
|
|
state = STOPPED;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Public Methods //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Retrieve the audio buffer size
|
|
public synchronized double getBuffer() {
|
|
return buffer;
|
|
}
|
|
|
|
// Retrieve the number of audio channels
|
|
public synchronized int getChannels() {
|
|
return channels;
|
|
}
|
|
|
|
// Retrieve the audio delay
|
|
public synchronized double getDelay() {
|
|
return delay;
|
|
}
|
|
|
|
// Calculate the number of audio frames per timer frame
|
|
public synchronized int getFrames() {
|
|
return audioFrames(rate);
|
|
}
|
|
|
|
// Retrieve the frame rate
|
|
public synchronized double getRate() {
|
|
return rate;
|
|
}
|
|
|
|
// Retrieve the audio sampling rate
|
|
public synchronized int getSampleRate() {
|
|
return sampleRate;
|
|
}
|
|
|
|
// Retrieve the timing source
|
|
public synchronized int getSource() {
|
|
return source;
|
|
}
|
|
|
|
// Retrieve the operation state
|
|
public synchronized int getState() {
|
|
return state;
|
|
}
|
|
|
|
// Pause the timer
|
|
public synchronized boolean pause() {
|
|
boolean isTimer = Thread.currentThread() == timer;
|
|
|
|
// The timer thread already owns the lock
|
|
if (isTimer && lock.isHeldByCurrentThread()) {
|
|
if (state == RUNNING)
|
|
state = PAUSED;
|
|
return state == PAUSED;
|
|
}
|
|
|
|
// Obtain the lock
|
|
lock.lock();
|
|
|
|
// Invalid state
|
|
if (state != RUNNING) {
|
|
lock.unlock();
|
|
return state == PAUSED;
|
|
}
|
|
|
|
// Configure state
|
|
state = PAUSED;
|
|
|
|
// Pause timer thread
|
|
if (!isTimer) try {
|
|
timer.interrupt();
|
|
lock.unlock();
|
|
cycTimer.await();
|
|
} catch (Exception e) { }
|
|
|
|
return true;
|
|
}
|
|
|
|
// Resume running the paused timer
|
|
public synchronized boolean resume() {
|
|
boolean isTimer = Thread.currentThread() == timer;
|
|
|
|
// The timer thread already owns the lock
|
|
if (isTimer && lock.isHeldByCurrentThread()) {
|
|
if (state == PAUSED)
|
|
state = RUNNING;
|
|
return state == RUNNING;
|
|
}
|
|
|
|
// Obtain the lock
|
|
lock.lock();
|
|
|
|
// Invalid state
|
|
if (state != PAUSED) {
|
|
lock.unlock();
|
|
return state == RUNNING;
|
|
}
|
|
|
|
// Configure state
|
|
state = RUNNING;
|
|
|
|
// Resume timer thread
|
|
if (!isTimer) try {
|
|
lock.unlock();
|
|
cycTimer.await();
|
|
} catch (Exception e) { }
|
|
|
|
return true;
|
|
}
|
|
|
|
// Specify the audio buffer length
|
|
public synchronized double setBuffer(double buffer) {
|
|
return this.buffer = state == STOPPED && buffer > 0 ?
|
|
buffer : this.buffer;
|
|
}
|
|
|
|
// Specify the callback handler
|
|
public synchronized boolean setCallback(Callback callback) {
|
|
if (state != STOPPED)
|
|
return false;
|
|
this.callback = callback;
|
|
return true;
|
|
}
|
|
|
|
// Specify the number of audio channels
|
|
public synchronized int setChannels(int channels) {
|
|
return this.channels = state == STOPPED && (channels - 1 & ~1) != 0 ?
|
|
channels : this.channels;
|
|
}
|
|
|
|
// Specify the audio delay
|
|
public synchronized double setDelay(double delay) {
|
|
return this.delay = state == STOPPED && delay > 0 ? delay : this.delay;
|
|
}
|
|
|
|
// Specify the frame rate
|
|
public synchronized double setRate(double rate) {
|
|
return this.rate = state == STOPPED && rate > 0 ? rate : this.rate;
|
|
}
|
|
|
|
// Specify the audio sampling rate
|
|
public synchronized int setSampleRate(int sampleRate) {
|
|
return this.sampleRate = state == STOPPED && sampleRate > 0 ?
|
|
sampleRate : this.sampleRate;
|
|
}
|
|
|
|
// Begin timer operations
|
|
public synchronized boolean start(int source) {
|
|
|
|
// Error checking
|
|
if (source != CLOCK && source != AUDIO || state != STOPPED ||
|
|
callback == null || Thread.currentThread() == timer ||
|
|
source == AUDIO && buffer > delay) {
|
|
return false;
|
|
}
|
|
|
|
// Audio processing
|
|
if (source == AUDIO) try {
|
|
|
|
// Open the output line
|
|
AudioFormat fmt =
|
|
new AudioFormat(sampleRate, 16, channels, true, false);
|
|
line = AudioSystem.getSourceDataLine(fmt);
|
|
line.open(fmt);
|
|
|
|
// Configure audio fields
|
|
frames = audioFrames(rate);
|
|
block = 0;
|
|
blocks = new byte[(int) Math.ceil(delay / rate)]
|
|
[frames * channels * 2];
|
|
}
|
|
|
|
// Could not open the audio line
|
|
catch (Exception e) {
|
|
line = null;
|
|
return false;
|
|
}
|
|
|
|
// Configure state
|
|
this.source = source;
|
|
state = RUNNING;
|
|
|
|
// Spawn and start timer thread
|
|
cycTimer.reset();
|
|
timer = new Thread(()->timer());
|
|
timer.setDaemon(true);
|
|
timer.start();
|
|
|
|
// Spawn and start audio thread
|
|
if (source == AUDIO) {
|
|
cycAudio.reset();
|
|
audio = new Thread(()->audio());
|
|
audio.setDaemon(true);
|
|
audio.start();
|
|
}
|
|
|
|
// Synchronize with timer thread
|
|
try { cycTimer.await(); } catch (Exception e) { }
|
|
|
|
return true;
|
|
}
|
|
|
|
// End timer operations
|
|
public synchronized boolean stop() {
|
|
boolean isTimer = Thread.currentThread() == timer;
|
|
|
|
// The timer thread already owns the lock
|
|
if (isTimer && lock.isHeldByCurrentThread())
|
|
return true;
|
|
|
|
// Obtain the lock
|
|
lock.lock();
|
|
|
|
// Invalid state
|
|
if (state == STOPPED) {
|
|
lock.unlock();
|
|
return true;
|
|
}
|
|
|
|
// Configure state
|
|
boolean paused = state == PAUSED;
|
|
state = STOPPED;
|
|
|
|
// Stop timer thread
|
|
if (!isTimer) try {
|
|
if (!paused) timer.interrupt();
|
|
lock.unlock();
|
|
if ( paused) cycTimer.await();
|
|
cycTimer.await();
|
|
} catch (Exception e) { }
|
|
|
|
return true;
|
|
}
|
|
|
|
// Write audio samples as bytes to output
|
|
public boolean write(byte[] samples, int offset) {
|
|
int size = frames * channels * 2;
|
|
|
|
// Error checking
|
|
if (Thread.currentThread() != timer || state == STOPPED || written ||
|
|
samples == null || offset < 0 || samples.length < offset + size)
|
|
return false;
|
|
|
|
// Add a new sample block to the queue
|
|
block = (block + 1) % blocks.length;
|
|
byte[] block = blocks[this.block];
|
|
System.arraycopy(samples, offset, block, 0, size);
|
|
queue.offer(block);
|
|
return written = true;
|
|
}
|
|
|
|
// Write audio samples as shorts to output
|
|
public boolean write(short[] samples, int offset) {
|
|
int size = frames * channels;
|
|
|
|
// Error checking
|
|
if (Thread.currentThread() != timer || state == STOPPED || written ||
|
|
samples == null || offset < 0 || samples.length < offset + size)
|
|
return false;
|
|
|
|
// Encode the samples as bytes
|
|
block = (block + 1) % blocks.length;
|
|
byte[] block = blocks[this.block];
|
|
for (int src = 0, dest = 0; src < size; src++) {
|
|
short sample = samples[offset + src];
|
|
block[dest++] = (byte) sample;
|
|
block[dest++] = (byte) (sample >> 8);
|
|
}
|
|
queue.offer(block);
|
|
return written = true;
|
|
}
|
|
|
|
// Write audio samples as floats to output (range -1 to +1)
|
|
public boolean write(float[] samples, int offset) {
|
|
int size = frames * channels;
|
|
|
|
// Error checking
|
|
if (Thread.currentThread() != timer || state == STOPPED || written ||
|
|
samples == null || offset < 0 || samples.length < offset + size)
|
|
return false;
|
|
|
|
// Encode the samples as bytes
|
|
block = (block + 1) % blocks.length;
|
|
byte[] block = blocks[this.block];
|
|
for (int src = 0, dest = 0; src < size; src++) {
|
|
short sample = (short) Math.round(32767 *
|
|
Math.min(1, Math.max(-1, samples[offset + src])) );
|
|
block[dest++] = (byte) sample;
|
|
block[dest++] = (byte) (sample >> 8);
|
|
}
|
|
queue.offer(block);
|
|
return written = true;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Private Methds //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Calculate the number of audio sampling frames in some number of seconds
|
|
private int audioFrames(double seconds) {
|
|
return Math.max(1, (int) Math.round(seconds * sampleRate));
|
|
}
|
|
|
|
// Handler for pause operations -- invoked by timer thread
|
|
private long onPause(long reference, boolean isCallback) {
|
|
|
|
// Track the current time
|
|
if (source == CLOCK)
|
|
reference = System.nanoTime() - reference;
|
|
|
|
// Pause audio thread
|
|
else try {
|
|
audio.interrupt();
|
|
line.stop();
|
|
cycAudio.await();
|
|
} catch (Exception e) { }
|
|
|
|
// Synchronization
|
|
try {
|
|
if (!isCallback)
|
|
cycTimer.await(); // Synchronize with invoking thread
|
|
else lock.unlock();
|
|
cycTimer.await(); // Wait for resume() or stop()
|
|
} catch (Exception e) { }
|
|
|
|
// Calculate a new reference time
|
|
if (source == CLOCK)
|
|
reference = System.nanoTime() - reference;
|
|
|
|
// Unpause audio thread
|
|
else try { cycAudio.await(); } catch (Exception e) { }
|
|
|
|
return reference;
|
|
}
|
|
|
|
// Handler for stop operations -- invoked by timer thread
|
|
private int onStop(boolean paused, boolean isCallback) {
|
|
|
|
// Stop the audio thread
|
|
if (source == AUDIO) try {
|
|
audio.interrupt();
|
|
line.stop();
|
|
cycAudio.await();
|
|
} catch (Exception e) { }
|
|
|
|
// Synchronization
|
|
try {
|
|
if (!isCallback || paused)
|
|
cycTimer.await(); // Synchronize with invoking thread
|
|
else lock.unlock();
|
|
} catch (Exception e) { }
|
|
|
|
// Cleanup
|
|
blocks = null;
|
|
timer = null;
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Thread Methods //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
// Audio thread entry point
|
|
private void audio() {
|
|
|
|
// Synchronize with timer thread
|
|
try { cycAudio.await(); } catch (Exception e) { }
|
|
|
|
// Initialize working variables
|
|
byte[] block = new byte[audioFrames(delay ) * channels * 2];
|
|
byte[] buffer = new byte[audioFrames(this.buffer) * channels * 2];
|
|
int blockPos = 0;
|
|
int bufferPos = -buffer.length; // Less than 0 means not full
|
|
|
|
// Audio processing
|
|
line.start();
|
|
for (;;) {
|
|
|
|
// Fill the buffer with samples, blocking until full
|
|
while (bufferPos < 0) {
|
|
|
|
// Load bytes from the current block
|
|
if (blockPos < block.length) {
|
|
int size = Math.min(block.length - blockPos, -bufferPos);
|
|
System.arraycopy(block, blockPos, buffer,
|
|
bufferPos + buffer.length, size);
|
|
blockPos += size;
|
|
bufferPos += size;
|
|
}
|
|
|
|
// Fetch a new sample block, blocking until one is available
|
|
else try {
|
|
block = queue.take();
|
|
blockPos = 0;
|
|
}
|
|
|
|
// The timer state has changed
|
|
catch (Exception e) {
|
|
audio.interrupt(); // take() clears interrupt status
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
// Send samples to the output, blocking until sent
|
|
if (!audio.isInterrupted()) {
|
|
bufferPos +=
|
|
line.write(buffer, bufferPos, buffer.length - bufferPos);
|
|
if (bufferPos == buffer.length)
|
|
bufferPos = -buffer.length;
|
|
}
|
|
|
|
// Check for changes to the timer state
|
|
if (state == RUNNING)
|
|
continue;
|
|
Thread.interrupted();
|
|
|
|
// Timer has paused
|
|
if (state == PAUSED) try {
|
|
cycAudio.await(); // Synchronize with timer thread
|
|
cycAudio.await(); // Wait for resume() or stop()
|
|
} catch (Exception e) { }
|
|
|
|
// Timer has stopped
|
|
if (state == STOPPED) {
|
|
try { line.close(); } catch (Exception e) { }
|
|
try { cycAudio.await(); } catch (Exception e) { }
|
|
audio = null;
|
|
return;
|
|
}
|
|
|
|
// Resume playback
|
|
line.start();
|
|
}
|
|
|
|
}
|
|
|
|
// Timer thread entry point
|
|
private int timer() {
|
|
|
|
// Synchronize with other threads
|
|
try {
|
|
if (source == AUDIO)
|
|
cycAudio.await(); // Synchronize with audio thread
|
|
cycTimer.await(); // Synchronize with parent thread
|
|
} catch (Exception e) { }
|
|
|
|
// Initialize working variables
|
|
byte[] empty = new byte[frames * channels * 2];
|
|
long reference = source == AUDIO ? 0 : System.nanoTime();
|
|
long target = source == AUDIO ? audioFrames(rate) :
|
|
Math.max(1, (long) Math.round(rate * 1000000000L));
|
|
|
|
// Timer processing
|
|
for (boolean first = true;; first = false) {
|
|
long current = source == CLOCK ? System.nanoTime() :
|
|
line.getLongFramePosition();
|
|
long remain = target - current + reference;
|
|
|
|
// Processing on all frames but the first
|
|
if (!first) {
|
|
|
|
// Wait until the next frame interval
|
|
if (remain > 0) try {
|
|
if (source == AUDIO)
|
|
remain = remain * 1000000000L / sampleRate;
|
|
remain = Math.max(1000000, remain);
|
|
Thread.sleep(remain / 1000000, (int) (remain % 1000000));
|
|
continue;
|
|
} catch (Exception e) { timer.interrupt(); }
|
|
|
|
// Another thread configured the timer state
|
|
if (Thread.interrupted()) {
|
|
if (state == PAUSED)
|
|
reference = onPause(reference, false);
|
|
if (state == STOPPED)
|
|
return onStop(false, false);
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
// Invoke the event callback
|
|
written = false;
|
|
callback.onFrame(this);
|
|
if (source == AUDIO && !written)
|
|
queue.offer(empty);
|
|
|
|
// The callback configured the timer state
|
|
if (lock.isHeldByCurrentThread()) {
|
|
boolean paused = state == PAUSED;
|
|
if (state == PAUSED)
|
|
reference = onPause(reference, true);
|
|
if (state == STOPPED)
|
|
return onStop(paused, true);
|
|
continue;
|
|
}
|
|
|
|
// Track processed frame time
|
|
reference = current + remain;
|
|
}
|
|
|
|
}
|
|
|
|
}
|