refactor: 用明确状态机重写 CapsLock 语音输入,彻底解决抖动

旧方案依赖多个布尔标志(capsResetDone_/cooldownActive_/longPressDetected_)
分散在各个处理函数中,复杂交互下容易产生竞态。

新方案使用明确四态状态机:
  Idle → PreRecording(按下) → Recording(1s后) → Cooldown(松开后)

核心防抖:
- Recording 状态下屏蔽所有 Activated 信号
- Cooldown 状态下屏蔽所有 Activated 信号
- PreRecording 状态下忽略重复 Activated

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alvin Young 2026-06-11 14:46:08 +08:00
parent 13d4aae725
commit 58a732e161
2 changed files with 67 additions and 70 deletions

View File

@ -40,34 +40,25 @@ VoiceInputService::VoiceInputService(ConfigManager* configManager,
{ {
impl_->sttEngine = sttEngine; impl_->sttEngine = sttEngine;
// 1s 定时器:启动录音 // 长按确认定时器1s
longPressTimer_ = new QTimer(this); longPressTimer_ = new QTimer(this);
longPressTimer_->setSingleShot(true); longPressTimer_->setSingleShot(true);
connect(longPressTimer_, &QTimer::timeout, this, [this]() { connect(longPressTimer_, &QTimer::timeout, this, [this]() {
if (!longPressDetected_) { if (state_ == PreRecording) {
longPressDetected_ = true; state_ = Recording;
emit statusChanged("正在录音..."); emit statusChanged("正在录音...");
LOG_DEBUG(kTag, "状态转换: PreRecording → Recording (长按确认)");
} }
}); });
// 3s 定时器:复位 CapsLock 灯(防抖:只在未按下状态下执行) // 松开后冷却定时器
capsResetTimer_ = new QTimer(this);
capsResetTimer_->setSingleShot(true);
connect(capsResetTimer_, &QTimer::timeout, this, [this]() {
// 如果已经松开recording_ = false不执行
if (recording_) {
capsResetDone_ = true;
simulateCapsLock();
LOG_DEBUG(kTag, "CapsLock 灯已复位(防抖保护中)");
}
});
// 松开后的冷却定时器
cooldownTimer_ = new QTimer(this); cooldownTimer_ = new QTimer(this);
cooldownTimer_->setSingleShot(true); cooldownTimer_->setSingleShot(true);
connect(cooldownTimer_, &QTimer::timeout, this, [this]() { connect(cooldownTimer_, &QTimer::timeout, this, [this]() {
cooldownActive_ = false; if (state_ == Cooldown) {
LOG_DEBUG(kTag, "冷却期结束,恢复 CapsLock 检测"); state_ = Idle;
LOG_DEBUG(kTag, "状态转换: Cooldown → Idle (冷却结束)");
}
}); });
} }
@ -112,6 +103,7 @@ bool VoiceInputService::start() {
} }
running_ = true; running_ = true;
state_ = Idle;
emit statusChanged("语音输入已启动(等待授权..."); emit statusChanged("语音输入已启动(等待授权...");
LOG_INFO(kTag, "语音输入服务已启动"); LOG_INFO(kTag, "语音输入服务已启动");
return true; return true;
@ -121,7 +113,6 @@ void VoiceInputService::stop() {
if (!running_) return; if (!running_) return;
longPressTimer_->stop(); longPressTimer_->stop();
capsResetTimer_->stop();
cooldownTimer_->stop(); cooldownTimer_->stop();
if (impl_->audioCapture) { if (impl_->audioCapture) {
@ -133,74 +124,84 @@ void VoiceInputService::stop() {
running_ = false; running_ = false;
recording_ = false; recording_ = false;
longPressDetected_ = false; state_ = Idle;
capsResetDone_ = false;
cooldownActive_ = false;
audioBuffer_.clear(); audioBuffer_.clear();
LOG_INFO(kTag, "语音输入服务已停止"); LOG_INFO(kTag, "语音输入服务已停止");
} }
void VoiceInputService::onHotkeyActivated() { void VoiceInputService::onHotkeyActivated() {
// CapsLock 已复位,用户仍按住键 → 忽略重复触发 // Recording 状态:屏蔽所有重复 Activated防抖核心
if (capsResetDone_) { if (state_ == Recording) {
LOG_DEBUG(kTag, "忽略重复的 ActivatedCapsLock 已复位,等待松开)"); LOG_DEBUG(kTag, "忽略 Activated (Recording 状态屏蔽)");
return; return;
} }
// 冷却期内忽略 // Cooldown 状态:冷却期内忽略
if (cooldownActive_) { if (state_ == Cooldown) {
LOG_DEBUG(kTag, "忽略 Activated(冷却期内)"); LOG_DEBUG(kTag, "忽略 Activated (冷却期内)");
return; return;
} }
LOG_DEBUG(kTag, "快捷键激活(按下)"); // Idle 状态 → 开始预录音
recording_ = true; if (state_ == Idle) {
longPressDetected_ = false; state_ = PreRecording;
capsResetDone_ = false; recording_ = true;
audioBuffer_.clear(); audioBuffer_.clear();
// 启动 1s 录音确认定时器 + 3s CapsLock 灯复位定时器 longPressTimer_->start(longPressThreshold_);
longPressTimer_->start(longPressThreshold_);
capsResetTimer_->start(capsResetDelayMs_);
// 开始音频采集(后台预采集) int deviceIndex = configManager_->get("audio.input_device").toInt();
int deviceIndex = configManager_->get("audio.input_device").toInt(); int sampleRate = configManager_->get("stt.sample_rate").toInt();
int sampleRate = configManager_->get("stt.sample_rate").toInt(); int bufferSizeMs = configManager_->get("audio.buffer_size_ms").toInt();
int bufferSizeMs = configManager_->get("audio.buffer_size_ms").toInt(); impl_->audioCapture->start(deviceIndex, sampleRate, bufferSizeMs);
impl_->audioCapture->start(deviceIndex, sampleRate, bufferSizeMs);
emit statusChanged("等待长按确认..."); LOG_DEBUG(kTag, "状态转换: Idle → PreRecording");
emit statusChanged("等待长按确认...");
return;
}
// PreRecording 状态收到重复 Activated → 忽略(防抖)
if (state_ == PreRecording) {
LOG_DEBUG(kTag, "忽略重复 Activated (PreRecording 防抖)");
return;
}
} }
void VoiceInputService::onHotkeyDeactivated() { void VoiceInputService::onHotkeyDeactivated() {
LOG_DEBUG(kTag, "快捷键停用(松开)"); LOG_DEBUG(kTag, QString("Deactivated (state=%1)").arg(recording_ ? "recording" : "idle"));
// Cooldown 状态的 Deactivated → 忽略
if (state_ == Cooldown) {
LOG_DEBUG(kTag, "忽略 Deactivated (Cooldown 状态屏蔽)");
return;
}
recording_ = false; recording_ = false;
longPressTimer_->stop(); longPressTimer_->stop();
capsResetTimer_->stop();
// 停止音频采集 // 停止音频采集
if (impl_->audioCapture && impl_->audioCapture->isRunning()) { if (impl_->audioCapture && impl_->audioCapture->isRunning()) {
impl_->audioCapture->stop(); impl_->audioCapture->stop();
} }
if (!longPressDetected_) { if (state_ == PreRecording) {
// 短按 → 模拟 CapsLock 按键 // 短按 → 模拟 CapsLock 切换大小写
state_ = Idle;
LOG_DEBUG(kTag, "短按,模拟 CapsLock"); LOG_DEBUG(kTag, "短按,模拟 CapsLock");
simulateCapsLock(); simulateCapsLock();
emit statusChanged("短按:切换 CapsLock"); emit statusChanged("短按:切换 CapsLock");
} else { } else if (state_ == Recording) {
// 长按 → CapsLock 已在 3s 定时器时复位,松开后直接开始识别 // 长按后松开 → 停止录音并转写
state_ = Idle;
LOG_DEBUG(kTag, "状态转换: Recording → Idle (松开转写)");
stopRecordingAndTranscribe(); stopRecordingAndTranscribe();
} }
longPressDetected_ = false; // 启动冷却期
capsResetDone_ = false; state_ = Cooldown;
// 启动冷却期1s 内忽略新的 Activated
cooldownActive_ = true;
cooldownTimer_->start(releaseCooldownMs_); cooldownTimer_->start(releaseCooldownMs_);
LOG_DEBUG(kTag, QString("冷却期启动 (%1ms)").arg(releaseCooldownMs_)); LOG_DEBUG(kTag, QString("状态转换: → Cooldown (%1ms)").arg(releaseCooldownMs_));
} }
void VoiceInputService::onAudioData(const std::vector<float>& samples, int sampleRate) { void VoiceInputService::onAudioData(const std::vector<float>& samples, int sampleRate) {
@ -259,7 +260,6 @@ void VoiceInputService::onRecognitionComplete(const QString& text) {
void VoiceInputService::simulateCapsLock() { void VoiceInputService::simulateCapsLock() {
if (impl_->injector && impl_->injector->isInitialized()) { if (impl_->injector && impl_->injector->isInitialized()) {
// XK_Caps_Lock = 0xffe5使用 simulateKeysym 自动转换为 keycode
impl_->injector->simulateKeysym(0xffe5); impl_->injector->simulateKeysym(0xffe5);
LOG_DEBUG(kTag, "模拟 CapsLock 按键已注入"); LOG_DEBUG(kTag, "模拟 CapsLock 按键已注入");
} else { } else {

View File

@ -17,12 +17,11 @@ class ConfigManager;
/** /**
* @brief CapsLock * @brief CapsLock
* *
* STT * CapsLock /
* * Idle
* 1. CapsLock * PreRecording CapsLock
* 2. 1s CapsLock * Recording 1s Portal
* 3. CapsLock * Cooldown
* 4. < CapsLock
*/ */
class VoiceInputService : public QObject { class VoiceInputService : public QObject {
Q_OBJECT Q_OBJECT
@ -60,25 +59,23 @@ private slots:
void onRecognitionComplete(const QString& text); void onRecognitionComplete(const QString& text);
private: private:
enum State { Idle, PreRecording, Recording, Cooldown };
State state_ = Idle;
struct Impl; struct Impl;
ConfigManager* configManager_ = nullptr; ConfigManager* configManager_ = nullptr;
std::unique_ptr<Impl> impl_; std::unique_ptr<Impl> impl_;
bool running_ = false; bool running_ = false;
bool recording_ = false; bool recording_ = false;
bool longPressDetected_ = false; int longPressThreshold_ = 1000;
bool capsResetDone_ = false; // CapsLock 复位后忽略重复 Activated int releaseCooldownMs_ = 1000;
bool cooldownActive_ = false; // 松开后的冷却期,防止立即重新触发
int longPressThreshold_ = 1000; // 1s 启动录音
int capsResetDelayMs_ = 3000; // 3s 后复位 CapsLock 灯
int releaseCooldownMs_ = 1000; // 松开后冷却时间
std::vector<float> audioBuffer_; std::vector<float> audioBuffer_;
int audioSampleRate_ = 16000; int audioSampleRate_ = 16000;
QTimer* longPressTimer_ = nullptr; // 1s 启动录音 QTimer* longPressTimer_ = nullptr;
QTimer* capsResetTimer_ = nullptr; // 3s 复位 CapsLock 灯 QTimer* cooldownTimer_ = nullptr;
QTimer* cooldownTimer_ = nullptr; // 松开后冷却
void startRecording(); void startRecording();
void stopRecordingAndTranscribe(); void stopRecordingAndTranscribe();