EnergyVad.java

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

/**
 * Local energy VAD on 16 kHz mono PCM16, analysed in 10 ms frames.
 *
 * <ul>
 * <li>status 0: no speech yet; 1: speaking; 2: speech ended</li>
 * <li>speech starts after {@link #START_FRAMES} consecutive voiced frames (RMS at or above the threshold)</li>
 * <li>speech ends after {@code seek} silent frames (10 ms units, default 60 = 600 ms); during the first
 * {@code refLength} frames after speech started the status stays 1</li>
 * <li>sound intensity 0 to 100 maps the frame RMS from -60 dBFS to 0 dBFS</li>
 * </ul>
 */
public final class EnergyVad {
    public static final int FRAME_BYTES = 320;
    public static final int START_FRAMES = 3;
    public static final int DEFAULT_SEEK = 60;
    /** RMS on the 16 bit scale counted as voice (about -38 dBFS). */
    public static final double DEFAULT_THRESHOLD = 400.0;

    private final int seekFrames;
    private final int refLengthFrames;
    private final double threshold;
    private final byte[] carry = new byte[FRAME_BYTES];
    private int carryLen;
    private int status;
    private int voicedRun;
    private int silentRun;
    private int speechFrames;
    private int intensity;
    private int peakIntensity;

    public EnergyVad(Integer seek, Integer refLength) {
        this(seek, refLength, DEFAULT_THRESHOLD);
    }

    public EnergyVad(Integer seek, Integer refLength, double threshold) {
        this.seekFrames = seek != null && seek > 0 ? seek : DEFAULT_SEEK;
        this.refLengthFrames = refLength != null && refLength > 0 ? refLength : 0;
        this.threshold = threshold;
    }

    public synchronized int status() {
        return status;
    }

    /** Intensity of the last complete frame. */
    public synchronized int intensity() {
        return intensity;
    }

    /** Highest intensity since the last call, then reset. */
    public synchronized int takePeakIntensity() {
        int p = peakIntensity;
        peakIntensity = 0;
        return p;
    }

    /** Analyses PCM16 little endian bytes; returns the status after the last complete frame. */
    public synchronized int process(byte[] data, int off, int len) {
        int pos = off;
        int end = off + len;
        if (carryLen > 0) {
            int take = Math.min(FRAME_BYTES - carryLen, len);
            System.arraycopy(data, pos, carry, carryLen, take);
            carryLen += take;
            pos += take;
            if (carryLen == FRAME_BYTES) {
                frame(carry, 0);
                carryLen = 0;
            }
        }
        while (pos + FRAME_BYTES <= end) {
            frame(data, pos);
            pos += FRAME_BYTES;
        }
        if (pos < end) {
            carryLen = end - pos;
            System.arraycopy(data, pos, carry, 0, carryLen);
        }
        return status;
    }

    private void frame(byte[] b, int off) {
        double rms = rms(b, off, FRAME_BYTES);
        intensity = intensityOf(rms);
        if (intensity > peakIntensity) {
            peakIntensity = intensity;
        }
        boolean voiced = rms >= threshold;
        if (status == 0) {
            voicedRun = voiced ? voicedRun + 1 : 0;
            if (voicedRun >= START_FRAMES) {
                status = 1;
                speechFrames = voicedRun;
                silentRun = 0;
            }
        } else if (status == 1) {
            speechFrames++;
            silentRun = voiced ? 0 : silentRun + 1;
            if (silentRun >= seekFrames && speechFrames > refLengthFrames) {
                status = 2;
            }
        }
    }

    /** RMS of PCM16 little endian samples. */
    public static double rms(byte[] b, int off, int len) {
        int samples = len / 2;
        if (samples == 0) {
            return 0;
        }
        double sum = 0;
        for (int i = 0; i < samples; i++) {
            int s = (short) ((b[off + 2 * i] & 0xFF) | (b[off + 2 * i + 1] << 8));
            sum += (double) s * s;
        }
        return Math.sqrt(sum / samples);
    }

    /** 0 to 100 for -60 dBFS to 0 dBFS. */
    public static int intensityOf(double rms) {
        if (rms <= 0) {
            return 0;
        }
        double dbfs = 20.0 * Math.log10(rms / 32768.0);
        double v = (dbfs + 60.0) * 100.0 / 60.0;
        if (v < 0) {
            return 0;
        }
        return v > 100 ? 100 : (int) Math.round(v);
    }
}