#include #include #include #include "vad.h" namespace { constexpr size_t kFrame = 320; // 20ms @16k constexpr double kPi = 3.14159265358979323846; // 生成 amplitude 幅值的 440Hz 正弦,用来模拟"有内容"的音频 std::vector tone(size_t samples, float amplitude) { std::vector v(samples); for (size_t i = 0; i < samples; ++i) { v[i] = amplitude * (float) std::sin(2.0 * kPi * 440.0 * (double) i / 16000.0); } return v; } int countSpeech(lc::Vad & vad, const std::vector & audio) { int speech = 0; for (size_t off = 0; off + kFrame <= audio.size(); off += kFrame) { if (vad.feed(audio.data() + off, kFrame)) ++speech; } return speech; } } // namespace // VAD 的回归测试。 // // 关键教训:噪声底必须用"最小值跟踪"。曾经用均值跟踪,遇到本来就很轻的音频 // (峰值 RMS 只有 0.03 量级的录音)会把阈值抬到语音电平之上,把绝大部分话音 // 判成静音 —— 实时字幕于是每句都被切断。 class TstVad : public QObject { Q_OBJECT private slots: void backgroundStaysSilent() { lc::Vad vad; const auto bg = tone(kFrame * 200, 0.001f); // rms ≈ 0.0007 QCOMPARE(countSpeech(vad, bg), 0); } // 轻语音必须被判成语音:这里语音 rms ≈ 0.0028, // 旧的"均值跟踪 + 0.004 绝对下限"实现会全部漏掉。 void quietSpeechIsDetected() { lc::Vad vad; const auto bg = tone(kFrame * 40, 0.001f); // rms ≈ 0.0007 countSpeech(vad, bg); const auto sp = tone(kFrame * 100, 0.004f); // rms ≈ 0.0028 const int speech = countSpeech(vad, sp); QVERIFY2(speech >= 95, qPrintable(QString("轻语音只被识别出 %1/100 帧").arg(speech))); } void loudSpeechIsDetected() { lc::Vad vad; const auto sp = tone(kFrame * 50, 0.2f); QVERIFY(countSpeech(vad, sp) >= 48); } void floorFollowsQuietInput() { lc::Vad vad; const auto bg = tone(kFrame * 200, 0.001f); countSpeech(vad, bg); // 噪声底应该已经下探到背景噪声量级,而不是停在初值 QVERIFY2(vad.noiseFloor() < 0.002f, qPrintable(QString("噪声底仍为 %1").arg(vad.noiseFloor()))); } }; QTEST_MAIN(TstVad) #include "tst_vad.moc"