#include "findLongestSilence.hpp" #include "androidUtil.h" static void highpass(std::vector& data, int sampleRate) { // Highpass filter. See https://en.wikipedia.org/wiki/High-pass_filter and // the example in whisper.cpp/streaming. float highpassCutoffHz = 60.0f; float RC = 1.0f / (2 * 3.1416f * highpassCutoffHz); float timePerSample = 1.0f / sampleRate; float alpha = RC / (RC + timePerSample); float lastInput = data[0]; for (int i = 1; i < data.size(); i++) { float currentInput = data[i]; data[i] = alpha * data[i - 1] + alpha * (currentInput - lastInput); lastInput = currentInput; } } SilenceRange findLongestSilence( const std::vector& audioData, LongestSilenceOptions options ) { // Options variables int sampleRate = options.sampleRate; int maxSilencePosition = options.maximumSilenceStartSamples; float minSilenceLengthSeconds = options.minSilenceLengthSeconds; bool returnFirstMatch = options.returnFirstMatch; // State int bestCandidateLength = 0; int bestCandidateStart = -1; int bestCandidateEnd = -1; int currentCandidateStart = -1; std::vector processedAudio { audioData }; highpass(processedAudio, sampleRate); // Break into windows of size `windowSize`: int windowSize = 256; int windowsPerSecond = sampleRate / windowSize; int quietWindows = 0; // Number of relatively quiet windows encountered // Finishes the current candidate for longest silence auto finalizeCandidate = [&] (int currentOffset) { bool hasCandidate = currentCandidateStart >= 0; if (!hasCandidate) { return; } int currentCandidateLength = currentOffset - currentCandidateStart; if (currentCandidateLength > bestCandidateLength && currentCandidateStart <= maxSilencePosition) { bestCandidateLength = currentCandidateLength; bestCandidateStart = currentCandidateStart; bestCandidateEnd = currentOffset; LOGD("New best candidate with length %d", currentCandidateLength); } currentCandidateStart = -1; }; int windowOffset; for (windowOffset = 0; windowOffset < processedAudio.size() && windowOffset <= maxSilencePosition; windowOffset += windowSize) { int rollingAverageSize = 24; float threshold = static_cast(rollingAverageSize) / 80.0f; // Count the number of samples that (when averaged with the nearby samples) // are below some threshold value. float absSum = 0; int silentSamples = 0; for (int i = windowOffset; i < windowOffset + windowSize && i < processedAudio.size(); i++) { absSum += abs(processedAudio[i]); bool isSumComplete = i - rollingAverageSize >= windowOffset; if (isSumComplete) { absSum -= abs(processedAudio[i - rollingAverageSize]); if (absSum < threshold) { silentSamples++; } } } // The window should be considered "quiet" if enough samples were below the threshold. // Don't require all of them to be to allow clicks and pops. if (silentSamples >= windowSize * 3 / 4) { quietWindows ++; } else { quietWindows = 0; } int minQuietWindows = static_cast(windowsPerSecond * minSilenceLengthSeconds); if (quietWindows >= minQuietWindows && currentCandidateStart == -1) { // Found silence // Ignore the first window, which probably contains some of the start of the audio // and the most recent window, which came after windowOffset. int windowsToIgnore = 2; int estimatedQuietSamples = std::max(0, quietWindows - windowsToIgnore) * windowSize; currentCandidateStart = windowOffset - estimatedQuietSamples; } else if (quietWindows == 0) { // Silence ended // Ended a candidate. Is it better than the best? finalizeCandidate(windowOffset); // Search for more candidates or return now? if (returnFirstMatch && bestCandidateLength > 0) { break; } } } finalizeCandidate(windowOffset); // Return the best candidate. if (bestCandidateLength == 0) { return { .isValid = false, .start = 0, .end = 0 }; } else { return { .isValid=true, .start=bestCandidateStart, .end=bestCandidateEnd }; } }