pvbemu/src/desktop/util/FrameTimer.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;
}
}
}