fix: 1s 后自动复位 CapsLock 灯,LED 作为录音状态指示器
按下 CapsLock → 灯亮 (PreRecording) → 1s 后灯灭 → 正式录音 (Recording) → 松开 → 识别 → 注入。Recording/Cooldown 状态屏蔽所有 Activated 信号防抖。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
88b0d57722
commit
277ac01f46
@ -40,14 +40,18 @@ 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 (state_ == PreRecording) {
|
if (state_ == PreRecording) {
|
||||||
|
// 复位 CapsLock 灯
|
||||||
|
simulateCapsLock();
|
||||||
|
// 进入正式录音
|
||||||
state_ = Recording;
|
state_ = Recording;
|
||||||
|
audioBuffer_.clear(); // 清除预录音期间的静音
|
||||||
emit statusChanged("正在录音...");
|
emit statusChanged("正在录音...");
|
||||||
LOG_DEBUG(kTag, "状态转换: PreRecording → Recording (长按确认)");
|
LOG_DEBUG(kTag, "PreRecording → Recording (灯灭,开始录音)");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,7 +61,7 @@ VoiceInputService::VoiceInputService(ConfigManager* configManager,
|
|||||||
connect(cooldownTimer_, &QTimer::timeout, this, [this]() {
|
connect(cooldownTimer_, &QTimer::timeout, this, [this]() {
|
||||||
if (state_ == Cooldown) {
|
if (state_ == Cooldown) {
|
||||||
state_ = Idle;
|
state_ = Idle;
|
||||||
LOG_DEBUG(kTag, "状态转换: Cooldown → Idle (冷却结束)");
|
LOG_DEBUG(kTag, "Cooldown → Idle (冷却结束)");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -74,7 +78,7 @@ bool VoiceInputService::start() {
|
|||||||
connect(impl_->audioCapture, &AudioCapture::audioDataReady,
|
connect(impl_->audioCapture, &AudioCapture::audioDataReady,
|
||||||
this, &VoiceInputService::onAudioData);
|
this, &VoiceInputService::onAudioData);
|
||||||
|
|
||||||
// 2. STT 引擎已作为参数传入(共享全局实例)
|
// 2. STT 引擎已作为参数传入
|
||||||
|
|
||||||
// 3. 初始化全局快捷键
|
// 3. 初始化全局快捷键
|
||||||
impl_->hotkey = new CapsLockVoiceHotkey(this);
|
impl_->hotkey = new CapsLockVoiceHotkey(this);
|
||||||
@ -96,7 +100,7 @@ bool VoiceInputService::start() {
|
|||||||
LOG_ERROR(kTag, "文本注入器初始化失败");
|
LOG_ERROR(kTag, "文本注入器初始化失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动快捷键(首次会弹出授权对话框)
|
// 启动快捷键
|
||||||
if (!impl_->hotkey->start()) {
|
if (!impl_->hotkey->start()) {
|
||||||
emit error("全局快捷键启动失败");
|
emit error("全局快捷键启动失败");
|
||||||
return false;
|
return false;
|
||||||
@ -104,7 +108,7 @@ bool VoiceInputService::start() {
|
|||||||
|
|
||||||
running_ = true;
|
running_ = true;
|
||||||
state_ = Idle;
|
state_ = Idle;
|
||||||
emit statusChanged("语音输入已启动(等待授权...)");
|
emit statusChanged("语音输入已启动");
|
||||||
LOG_INFO(kTag, "语音输入服务已启动");
|
LOG_INFO(kTag, "语音输入服务已启动");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -131,49 +135,39 @@ void VoiceInputService::stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VoiceInputService::onHotkeyActivated() {
|
void VoiceInputService::onHotkeyActivated() {
|
||||||
// Recording 状态:屏蔽所有重复 Activated(防抖核心)
|
// Recording 和 Cooldown 状态:屏蔽所有 Activated(防抖核心)
|
||||||
if (state_ == Recording) {
|
if (state_ == Recording || state_ == Cooldown) {
|
||||||
LOG_DEBUG(kTag, "忽略 Activated (Recording 状态屏蔽)");
|
LOG_DEBUG(kTag, QString("忽略 Activated (state=%1)").arg(state_ == Recording ? "Recording" : "Cooldown"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cooldown 状态:冷却期内忽略
|
// PreRecording 重复触发:忽略
|
||||||
if (state_ == Cooldown) {
|
if (state_ == PreRecording) {
|
||||||
LOG_DEBUG(kTag, "忽略 Activated (冷却期内)");
|
LOG_DEBUG(kTag, "忽略重复 Activated (PreRecording 防抖)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle 状态 → 开始预录音
|
// Idle → PreRecording(灯亮,预录音)
|
||||||
if (state_ == Idle) {
|
|
||||||
state_ = PreRecording;
|
state_ = PreRecording;
|
||||||
recording_ = true;
|
recording_ = true;
|
||||||
audioBuffer_.clear();
|
audioBuffer_.clear();
|
||||||
|
|
||||||
longPressTimer_->start(longPressThreshold_);
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
LOG_DEBUG(kTag, "状态转换: Idle → PreRecording");
|
// 启动 1s 定时器:灯灭 → 正式录音
|
||||||
emit statusChanged("等待长按确认...");
|
longPressTimer_->start(longPressThreshold_);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreRecording 状态收到重复 Activated → 忽略(防抖)
|
LOG_DEBUG(kTag, "Idle → PreRecording (灯亮)");
|
||||||
if (state_ == PreRecording) {
|
emit statusChanged("等待长按确认...");
|
||||||
LOG_DEBUG(kTag, "忽略重复 Activated (PreRecording 防抖)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoiceInputService::onHotkeyDeactivated() {
|
void VoiceInputService::onHotkeyDeactivated() {
|
||||||
LOG_DEBUG(kTag, QString("Deactivated (state=%1)").arg(recording_ ? "recording" : "idle"));
|
|
||||||
|
|
||||||
// Cooldown 状态的 Deactivated → 忽略
|
// Cooldown 状态的 Deactivated → 忽略
|
||||||
if (state_ == Cooldown) {
|
if (state_ == Cooldown) {
|
||||||
LOG_DEBUG(kTag, "忽略 Deactivated (Cooldown 状态屏蔽)");
|
LOG_DEBUG(kTag, "忽略 Deactivated (Cooldown)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,23 +180,23 @@ void VoiceInputService::onHotkeyDeactivated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state_ == PreRecording) {
|
if (state_ == PreRecording) {
|
||||||
// 短按 → 模拟 CapsLock 切换大小写
|
// 短按 → 恢复 CapsLock 灯
|
||||||
state_ = Idle;
|
|
||||||
LOG_DEBUG(kTag, "短按,模拟 CapsLock");
|
|
||||||
simulateCapsLock();
|
simulateCapsLock();
|
||||||
|
state_ = Idle;
|
||||||
|
LOG_DEBUG(kTag, "短按,恢复 CapsLock 灯");
|
||||||
emit statusChanged("短按:切换 CapsLock");
|
emit statusChanged("短按:切换 CapsLock");
|
||||||
} else if (state_ == Recording) {
|
} else if (state_ == Recording) {
|
||||||
// 长按后松开 → 先复位 CapsLock,再开始识别
|
// 长按后松开 → 先恢复 CapsLock,再开始识别
|
||||||
state_ = Idle;
|
|
||||||
LOG_DEBUG(kTag, "状态转换: Recording → Idle (松开转写)");
|
|
||||||
simulateCapsLock();
|
simulateCapsLock();
|
||||||
|
state_ = Idle;
|
||||||
|
LOG_DEBUG(kTag, "Recording → Idle (松开转写)");
|
||||||
stopRecordingAndTranscribe();
|
stopRecordingAndTranscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动冷却期
|
// 启动冷却期
|
||||||
state_ = Cooldown;
|
state_ = Cooldown;
|
||||||
cooldownTimer_->start(releaseCooldownMs_);
|
cooldownTimer_->start(releaseCooldownMs_);
|
||||||
LOG_DEBUG(kTag, QString("状态转换: → Cooldown (%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) {
|
||||||
@ -262,7 +256,7 @@ void VoiceInputService::onRecognitionComplete(const QString& text) {
|
|||||||
void VoiceInputService::simulateCapsLock() {
|
void VoiceInputService::simulateCapsLock() {
|
||||||
if (impl_->injector && impl_->injector->isInitialized()) {
|
if (impl_->injector && impl_->injector->isInitialized()) {
|
||||||
impl_->injector->simulateKeysym(0xffe5);
|
impl_->injector->simulateKeysym(0xffe5);
|
||||||
LOG_DEBUG(kTag, "模拟 CapsLock 按键已注入");
|
LOG_DEBUG(kTag, "模拟 CapsLock 按键");
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING(kTag, "文本注入器未初始化,无法模拟 CapsLock");
|
LOG_WARNING(kTag, "文本注入器未初始化,无法模拟 CapsLock");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,15 @@ class ConfigManager;
|
|||||||
/**
|
/**
|
||||||
* @brief CapsLock 语音输入服务
|
* @brief CapsLock 语音输入服务
|
||||||
*
|
*
|
||||||
* 状态机(防止 CapsLock 抖动/误触):
|
* CapsLock 灯作为录音状态指示器:
|
||||||
* Idle — 空闲,等待按键
|
* 按下 → 灯亮 (PreRecording) → 1s 后灯灭 → 正式录音 (Recording)
|
||||||
* PreRecording — 按下 CapsLock,预录音,等待长按确认
|
* → 松开 → 识别 → 注入 → 复位 CapsLock 状态
|
||||||
* Recording — 长按 1s 确认,正式录音(屏蔽所有 Portal 信号)
|
*
|
||||||
* Cooldown — 松开后冷却期,防止立即重新触发
|
* 状态机:
|
||||||
|
* Idle — 空闲
|
||||||
|
* PreRecording — 按下,灯亮,等待长按确认
|
||||||
|
* Recording — 1s 后灯灭,正式录音(屏蔽 Portal 信号)
|
||||||
|
* Cooldown — 松开后冷却,防止误触
|
||||||
*/
|
*/
|
||||||
class VoiceInputService : public QObject {
|
class VoiceInputService : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -31,19 +35,12 @@ public:
|
|||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
~VoiceInputService() override;
|
~VoiceInputService() override;
|
||||||
|
|
||||||
/** @brief 启动服务(初始化所有组件) */
|
|
||||||
bool start();
|
bool start();
|
||||||
|
|
||||||
/** @brief 停止服务 */
|
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
/** @brief 是否已启动 */
|
|
||||||
bool isRunning() const { return running_; }
|
bool isRunning() const { return running_; }
|
||||||
|
|
||||||
/** @brief 是否正在录音 */
|
|
||||||
bool isRecording() const { return recording_; }
|
bool isRecording() const { return recording_; }
|
||||||
|
|
||||||
/** @brief 长按阈值(毫秒),默认 1000ms */
|
|
||||||
void setLongPressThreshold(int ms) { longPressThreshold_ = ms; }
|
void setLongPressThreshold(int ms) { longPressThreshold_ = ms; }
|
||||||
int longPressThreshold() const { return longPressThreshold_; }
|
int longPressThreshold() const { return longPressThreshold_; }
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user