Accepting the settings dialog quit the app: with a tray-resident program "last window closed" must not mean "quit", but QApplication's default made it so, and on first run the dialog is the only window. Disable quitOnLastWindowClosed. Also fix listOutputDevices hanging forever (its callbacks never signalled the PulseAudio main loop), enlarge the caption window's close button, and add capture-device selection (enumerated sinks, settings combo, --list-devices/--device) plus model integrity verification (size + sha256 in the registry, --verify-model and a "校验完整性" button). 点「确定」后程序自己退出:本程序常驻托盘,"最后一个窗口关闭"不该等于退出, 但 QApplication 默认如此,而首次启动时设置对话框正是唯一窗口。已关闭该默认 行为。同时修复 listOutputDevices 永久卡死(回调没有 signal 主循环)、放大 字幕窗关闭按钮,并新增采集设备选择与模型完整性校验(注册表含 size/sha256, 提供 --verify-model 与设置里的「校验完整性」按钮)。
149 lines
4.1 KiB
C++
149 lines
4.1 KiB
C++
#include "livepipeline.h"
|
|
|
|
#include <chrono>
|
|
|
|
namespace lc {
|
|
|
|
namespace {
|
|
constexpr int kFrameSamples = 320; // 20ms @16k
|
|
} // namespace
|
|
|
|
LivePipeline::LivePipeline(IAsr & asr, LivePipelineOptions opt)
|
|
: asr_(asr), opt_(std::move(opt)), stream_(asr, opt_.stream) {}
|
|
|
|
LivePipeline::~LivePipeline()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
std::string LivePipeline::deviceName() const
|
|
{
|
|
return capture_.deviceName();
|
|
}
|
|
|
|
bool LivePipeline::start(std::string * error)
|
|
{
|
|
const bool ok = capture_.start(
|
|
asr_.sampleRate(),
|
|
[this](const float * s, size_t n) {
|
|
{
|
|
std::lock_guard<std::mutex> lk(mutex_);
|
|
pending_.insert(pending_.end(), s, s + n);
|
|
}
|
|
cv_.notify_one();
|
|
},
|
|
error,
|
|
opt_.captureDevice);
|
|
if (!ok) return false;
|
|
|
|
worker_ = std::thread([this] { workerLoop(); });
|
|
return true;
|
|
}
|
|
|
|
void LivePipeline::stop()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lk(mutex_);
|
|
quit_ = true;
|
|
}
|
|
cv_.notify_all();
|
|
if (worker_.joinable()) worker_.join();
|
|
capture_.stop();
|
|
}
|
|
|
|
void LivePipeline::workerLoop()
|
|
{
|
|
for (;;) {
|
|
std::vector<float> chunk;
|
|
{
|
|
std::unique_lock<std::mutex> lk(mutex_);
|
|
cv_.wait_for(lk, std::chrono::milliseconds(50),
|
|
[this] { return quit_ || !pending_.empty(); });
|
|
if (quit_ && pending_.empty()) break;
|
|
chunk.swap(pending_);
|
|
}
|
|
if (!chunk.empty()) process(chunk.data(), chunk.size());
|
|
}
|
|
|
|
if (inSegment_) finishSegment();
|
|
}
|
|
|
|
void LivePipeline::emitStep(const StreamingAsr::Step & s)
|
|
{
|
|
if (!s.ok || !onCaption_) return;
|
|
onCaption_(s.committed, s.tail, s.language);
|
|
}
|
|
|
|
void LivePipeline::process(const float * pcm, size_t n)
|
|
{
|
|
buffer_.insert(buffer_.end(), pcm, pcm + n);
|
|
|
|
const size_t prerollSamples = (size_t) (opt_.prerollSec * asr_.sampleRate());
|
|
const float frameSec = (float) kFrameSamples / asr_.sampleRate();
|
|
|
|
std::vector<float> segAudio; // 本段要送进引擎的音频
|
|
bool flushNow = false;
|
|
|
|
size_t off = 0;
|
|
for (; off + kFrameSamples <= buffer_.size(); off += kFrameSamples) {
|
|
const float * frame = buffer_.data() + off;
|
|
const bool speech = vad_.feed(frame, kFrameSamples);
|
|
|
|
if (!inSegment_) {
|
|
// 静音期间什么都不送给模型:否则模型会对静音产生幻觉
|
|
if (speech) {
|
|
inSegment_ = true;
|
|
silenceAcc_ = 0.0f;
|
|
segAudio.insert(segAudio.end(), preroll_.begin(), preroll_.end());
|
|
preroll_.clear();
|
|
segAudio.insert(segAudio.end(), frame, frame + kFrameSamples);
|
|
} else {
|
|
preroll_.insert(preroll_.end(), frame, frame + kFrameSamples);
|
|
if (preroll_.size() > prerollSamples) {
|
|
preroll_.erase(preroll_.begin(),
|
|
preroll_.begin() + (long) (preroll_.size() - prerollSamples));
|
|
}
|
|
}
|
|
} else {
|
|
segAudio.insert(segAudio.end(), frame, frame + kFrameSamples);
|
|
if (speech) silenceAcc_ = 0.0f;
|
|
else silenceAcc_ += frameSec;
|
|
|
|
if (silenceAcc_ >= (float) opt_.silenceTimeoutSec) {
|
|
flushNow = true;
|
|
off += kFrameSamples; // 这一帧已经并入 segAudio
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!segAudio.empty()) {
|
|
const auto cb = [this](const StreamingAsr::Step & s) { emitStep(s); };
|
|
stream_.feed(segAudio.data(), segAudio.size(), cb);
|
|
}
|
|
|
|
if (flushNow) {
|
|
const auto cb = [this](const StreamingAsr::Step & s) { emitStep(s); };
|
|
stream_.flush(cb);
|
|
stream_.reset();
|
|
inSegment_ = false;
|
|
silenceAcc_ = 0.0f;
|
|
preroll_.clear();
|
|
}
|
|
|
|
if (off > 0) buffer_.erase(buffer_.begin(), buffer_.begin() + (long) off);
|
|
}
|
|
|
|
void LivePipeline::finishSegment()
|
|
{
|
|
if (!inSegment_) return;
|
|
const auto cb = [this](const StreamingAsr::Step & s) { emitStep(s); };
|
|
stream_.flush(cb);
|
|
stream_.reset();
|
|
inSegment_ = false;
|
|
silenceAcc_ = 0.0f;
|
|
preroll_.clear();
|
|
}
|
|
|
|
} // namespace lc
|