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 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(); 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; } } }