Capture.java

// SPDX-License-Identifier: Apache-2.0
package com.shengzhiai.yugu.stcompat.internal;

/**
 * Capture thread reading an {@link AudioInput} in 50 ms chunks. Pause stops the device (the
 * microphone indicator goes off) and resume starts it again. The device is released by the
 * capture thread itself once the loop has ended, so a blocking read never races a release.
 */
public final class Capture {
    /** Chunk size: 50 ms of 16 kHz mono PCM16. */
    public static final int CHUNK_BYTES = Wav.BYTES_PER_SECOND / 20;

    /** Receives chunks on the capture thread. */
    public interface Sink {
        void onPcm(byte[] data, int length);

        void onCaptureError(String reason);
    }

    private final AudioInput input;
    private final Sink sink;
    private final Thread thread;
    private final Object lock = new Object();
    private boolean running = true;
    private boolean paused;
    private volatile boolean mute;
    private boolean finished;

    public Capture(AudioInput input, Sink sink, String name) {
        this.input = input;
        this.sink = sink;
        this.thread = new Thread(new Runnable() {
            @Override
            public void run() {
                loop();
            }
        }, name);
        this.thread.setDaemon(true);
    }

    /** Starts the device and the thread; releases the device when it cannot be started. */
    public void start() throws AudioInput.AudioInputException {
        startDevice();
        startThread();
    }

    /** Starts the device only; audio is buffered by the device until {@link #startThread()}. */
    public void startDevice() throws AudioInput.AudioInputException {
        try {
            input.start();
        } catch (AudioInput.AudioInputException e) {
            input.release();
            synchronized (lock) {
                running = false;
                finished = true;
                lock.notifyAll();
            }
            throw e;
        }
    }

    /** Starts reading on the capture thread. */
    public void startThread() {
        thread.start();
    }

    public void setMute(boolean m) {
        mute = m;
    }

    public void pause() {
        synchronized (lock) {
            if (!running || paused) {
                return;
            }
            paused = true;
        }
        input.stop();
    }

    /** Resumes after {@link #pause()}; returns false when the device cannot be restarted. */
    public boolean resume() {
        synchronized (lock) {
            if (!running || !paused) {
                return running;
            }
        }
        try {
            input.start();
        } catch (AudioInput.AudioInputException e) {
            YLog.w("microphone could not be restarted: " + e.getMessage());
            return false;
        }
        synchronized (lock) {
            paused = false;
            lock.notifyAll();
        }
        return true;
    }

    /** Requests the end of capture; does not block. */
    public void stop() {
        synchronized (lock) {
            if (!running) {
                return;
            }
            running = false;
            lock.notifyAll();
        }
        input.stop();
    }

    /** Waits for the capture thread to finish; returns false on timeout. */
    public boolean await(long timeoutMs) {
        long end = System.currentTimeMillis() + timeoutMs;
        synchronized (lock) {
            while (!finished) {
                long left = end - System.currentTimeMillis();
                if (left <= 0) {
                    return false;
                }
                try {
                    lock.wait(left);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }
        return true;
    }

    private void loop() {
        byte[] buf = new byte[CHUNK_BYTES];
        try {
            while (true) {
                synchronized (lock) {
                    while (running && paused) {
                        try {
                            lock.wait(200);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            running = false;
                        }
                    }
                    if (!running) {
                        break;
                    }
                }
                int n = input.read(buf, 0, buf.length);
                if (n < 0) {
                    boolean active;
                    synchronized (lock) {
                        active = running && !paused;
                    }
                    if (active) {
                        sink.onCaptureError("microphone read failed with code " + n);
                        break;
                    }
                    continue;
                }
                if (n == 0) {
                    sleepQuietly();
                    continue;
                }
                synchronized (lock) {
                    if (paused && running) {
                        continue;
                    }
                }
                if (mute) {
                    java.util.Arrays.fill(buf, 0, n, (byte) 0);
                }
                sink.onPcm(buf, n);
            }
        } catch (RuntimeException e) {
            YLog.e("capture failed", e);
            sink.onCaptureError("capture failed: " + e.getMessage());
        } finally {
            input.release();
            synchronized (lock) {
                running = false;
                finished = true;
                lock.notifyAll();
            }
        }
    }

    private static void sleepQuietly() {
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}