Ariver
2026-05-18 db6aabfb7aa4460c8858fec0a51534ebe34786f7
VoiceSnapGo/app.go
@@ -26,8 +26,8 @@
)
const (
   appVersion        = "2.1.1"
   appBuild          = "20260518.1541"
   appVersion        = "2.1.3"
   appBuild          = "20260518.1651"
   appDisplayVersion = appVersion + " (build " + appBuild + ")"
   appName           = "VoiceSnap"
@@ -249,9 +249,10 @@
      a.hotkeyPollSeen = true
      a.lastHotkeyDown = isDown
      logger.Info(
         "Hotkey poll state: key=%s vk=0x%X down=%t active=%t recording=%t freetalk=%t engineReady=%t recordingHotkey=%t",
         "Hotkey poll state: key=%s vk=0x%X mode=%s down=%t active=%t recording=%t freetalk=%t engineReady=%t recordingHotkey=%t",
         hotkey.GetKeyName(a.cfg.HotkeyVK),
         a.cfg.HotkeyVK,
         config.NormalizeHotkeyMode(a.cfg.HotkeyMode),
         isDown,
         a.hotkeyActive,
         a.isRecording,
@@ -284,18 +285,27 @@
      return
   }
   switch config.NormalizeHotkeyMode(a.cfg.HotkeyMode) {
   case config.HotkeyModeTap:
      a.pollTapHotkeyLocked(isDown)
   default:
      a.pollHoldHotkeyLocked(isDown)
   }
}
func (a *App) pollHoldHotkeyLocked(isDown bool) {
   if isDown {
      if !a.hotkeyActive {
         // Key just pressed
         a.hotkeyActive = true
         a.isCombination = false
         a.hotkeyPressTime = time.Now()
         logger.Info("Hotkey accepted: key=%s vk=0x%X freetalk=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.cfg.HotkeyVK, a.isFreetalking)
         logger.Info("Hotkey accepted: key=%s vk=0x%X mode=%s freetalk=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.cfg.HotkeyVK, config.HotkeyModeHold, a.isFreetalking)
         // If in free talk mode, stop on next press
         // If the mode changed while a tap recording was active, let the next press stop it.
         if a.isFreetalking {
            logger.Info("Hotkey action: stop free talk")
            a.stopFreetalkLocked()
            logger.Info("Hotkey action: stop tap recording after mode switch")
            a.stopTapRecordingLocked()
            return
         }
      } else {
@@ -317,17 +327,54 @@
   } else if a.hotkeyActive {
      // Key released
      pressDuration := time.Since(a.hotkeyPressTime)
      logger.Info("Hotkey released: key=%s duration=%dms recording=%t combination=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), pressDuration.Milliseconds(), a.isRecording, a.isCombination)
      logger.Info("Hotkey released: key=%s mode=%s duration=%dms recording=%t combination=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), config.HotkeyModeHold, pressDuration.Milliseconds(), a.isRecording, a.isCombination)
      a.hotkeyActive = false
      if a.isRecording {
         // Hold-to-talk: release stops recording
         a.stopRecordingLocked(a.isCombination)
      } else if !a.isCombination && pressDuration < 300*time.Millisecond && time.Since(a.lastStopTime) > 500*time.Millisecond {
         // Short tap (<300ms): start free talk mode
         logger.Info("Hotkey action: start free talk after short tap")
         a.startFreetalkLocked()
      }
   }
}
func (a *App) pollTapHotkeyLocked(isDown bool) {
   if isDown {
      if !a.hotkeyActive {
         a.hotkeyActive = true
         a.isCombination = false
         a.hotkeyPressTime = time.Now()
         logger.Info("Hotkey accepted: key=%s vk=0x%X mode=%s recording=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), a.cfg.HotkeyVK, config.HotkeyModeTap, a.isFreetalking)
         return
      }
      if !a.isCombination && a.hk.IsAnyOtherKeyPressedSince(a.cfg.HotkeyVK, a.hotkeyPressTime) {
         a.isCombination = true
         logger.Info("Hotkey marked as combination: key=%s mode=%s", hotkey.GetKeyName(a.cfg.HotkeyVK), config.HotkeyModeTap)
      }
      return
   }
   if !a.hotkeyActive {
      return
   }
   pressDuration := time.Since(a.hotkeyPressTime)
   logger.Info("Hotkey released: key=%s mode=%s duration=%dms recording=%t combination=%t", hotkey.GetKeyName(a.cfg.HotkeyVK), config.HotkeyModeTap, pressDuration.Milliseconds(), a.isRecording, a.isCombination)
   a.hotkeyActive = false
   if a.isCombination {
      return
   }
   if a.isFreetalking {
      logger.Info("Hotkey action: stop tap recording")
      a.stopTapRecordingLocked()
      return
   }
   if time.Since(a.lastStopTime) > 500*time.Millisecond {
      logger.Info("Hotkey action: start tap recording")
      a.startTapRecordingLocked()
   }
}
@@ -379,7 +426,7 @@
   go a.recognizeAndPaste(hasVoice, samples)
}
func (a *App) startFreetalkLocked() {
func (a *App) startTapRecordingLocked() {
   if a.isRecording || a.isFreetalking {
      return
   }
@@ -389,7 +436,7 @@
   a.silenceSince = time.Time{}
   a.hideGen.Add(1)
   logger.Info("Free talk started")
   logger.Info("Tap recording started")
   a.positionIndicator()
   a.indicator.SetStatus(overlay.StatusFreetalking, "0:00")
   a.indicator.Show()
@@ -398,7 +445,7 @@
   }
   if err := a.recorder.Start(); err != nil {
      logger.Error("Failed to start free talk recording: %v", err)
      logger.Error("Failed to start tap recording: %v", err)
      a.isFreetalking = false
      a.isRecording = false
      return
@@ -431,7 +478,7 @@
   }()
}
func (a *App) stopFreetalkLocked() {
func (a *App) stopTapRecordingLocked() {
   if !a.isFreetalking {
      return
   }
@@ -442,19 +489,20 @@
   hasVoice := a.recorder.HasVoiceActivity()
   samples := a.recorder.StopAndGetSamples()
   logger.Info("Free talk stopped")
   logger.Info("Tap recording stopped")
   a.indicator.SetStatus(overlay.StatusProcessing, "识别中")
   go a.recognizeAndPaste(hasVoice, samples)
}
// checkSilenceTimeout monitors volume during free-talk mode and auto-stops
// recording if silence persists for silenceTimeoutDuration.
// checkSilenceTimeout monitors legacy free-talk recordings. The explicit tap
// mode is user-stopped by pressing the hotkey again, so it does not auto-stop
// on silence.
// Called from the audio volume callback (~every 50ms).
func (a *App) checkSilenceTimeout(vol float64) {
   a.mu.Lock()
   defer a.mu.Unlock()
   if !a.isFreetalking {
   if !a.isFreetalking || config.NormalizeHotkeyMode(a.cfg.HotkeyMode) == config.HotkeyModeTap {
      a.silenceSince = time.Time{}
      return
   }
@@ -502,7 +550,7 @@
}
// recognizeAndPaste runs ASR on the recorded samples and pastes the result.
// Shared by both hold-to-talk and free-talk modes.
// Shared by both hold and tap modes.
func (a *App) recognizeAndPaste(hasVoice bool, samples []float32) {
   // Snapshot config values under lock to avoid data races
   a.mu.Lock()
@@ -631,6 +679,7 @@
   // Show indicator briefly — snapshot config under lock
   a.mu.Lock()
   hotkeyVK := a.cfg.HotkeyVK
   hotkeyMode := config.NormalizeHotkeyMode(a.cfg.HotkeyMode)
   autoHide := a.cfg.AutoHide
   indX := a.cfg.IndicatorX
   indY := a.cfg.IndicatorY
@@ -638,11 +687,18 @@
   keyName := hotkey.GetKeyName(hotkeyVK)
   a.positionIndicatorAt(indX, indY)
   a.indicator.SetStatus(overlay.StatusReady, "按住"+keyName+"说话")
   a.indicator.SetStatus(overlay.StatusReady, hotkeyReadyText(keyName, hotkeyMode))
   a.indicator.Show()
   a.delayedHideIf(autoHide, 2000)
}
func hotkeyReadyText(keyName, mode string) string {
   if mode == config.HotkeyModeTap {
      return "点按" + keyName + "说话"
   }
   return "按住" + keyName + "说话"
}
// positionIndicator places the indicator at the saved position, or bottom-center if not saved.
// Caller must hold a.mu (reads a.cfg).
func (a *App) positionIndicator() {