Ariver
2026-05-18 14f801be6ea478678209c381fd188d7a58eadf93
fix: avoid recorder stop deadlock
4 files modified
50 ■■■■ changed files
TODO.md 9 ●●●●● patch | view | raw | blame | history
VoiceSnapGo/app.go 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/build/darwin/Info.plist 2 ●●● patch | view | raw | blame | history
VoiceSnapGo/internal/audio/recorder.go 37 ●●●● patch | view | raw | blame | history
TODO.md
@@ -17,6 +17,15 @@
## Done
- [2026-05-18] 修复停止录音时 `ma_device_stop` 与音频回调互等导致 App 假死的问题。
  - 根因: `Recorder.StopAndGetSamples()` 持有录音互斥锁调用 `device.Stop()`;底层 `ma_device_stop` 等待音频回调结束,而音频回调同时等待同一把锁,形成互等。
  - 修复: 停止录音时先在锁内摘除设备并标记非录音,释放锁后再调用 `Stop/Uninit`,避免底层停止设备时阻塞音频回调。
  - 增加停止设备过程日志: `Stopping audio device`、`Audio device stopped`、`Audio device uninitialized`。
  - build: `20260518.0125`
  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap.app`
  - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-build20260518.0125-arm64-local.dmg`
  - 兼容输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-arm64-local.dmg` 已替换为同一份新包。
  - 已安装到 `/Applications/VoiceSnap.app`,并在日志中验证一次完整停止链路。
- [2026-05-17] 放大菜单栏图标的有效绘制区域,改善深色菜单栏里图标过小的问题。
  - `VoiceSnapGo/build/darwin/tray_icon.png` 的有效内容区域从约 `156x156` 放大到约 `220x220`。
  - build: `20260517.2306`
VoiceSnapGo/app.go
@@ -27,7 +27,7 @@
const (
    appVersion        = "2.1.1"
    appBuild          = "20260517.2306"
    appBuild          = "20260518.0125"
    appDisplayVersion = appVersion + " (build " + appBuild + ")"
    appName           = "VoiceSnap"
VoiceSnapGo/build/darwin/Info.plist
@@ -17,7 +17,7 @@
    <key>CFBundleShortVersionString</key>
    <string>2.1.1</string>
    <key>CFBundleVersion</key>
    <string>20260517.2306</string>
    <string>20260518.0125</string>
    <key>LSMinimumSystemVersion</key>
    <string>11.0</string>
    <key>NSMicrophoneUsageDescription</key>
VoiceSnapGo/internal/audio/recorder.go
@@ -151,19 +151,21 @@
// Stop stops recording and discards audio data.
func (r *Recorder) Stop() {
    device := r.detachDevice()
    stopAndUninitDevice(device)
    r.mu.Lock()
    defer r.mu.Unlock()
    r.stopDevice()
    r.pcmBuf = nil
}
// StopAndGetSamples stops recording and returns the captured audio as float32 samples.
func (r *Recorder) StopAndGetSamples() []float32 {
    device := r.detachDevice()
    stopAndUninitDevice(device)
    r.mu.Lock()
    defer r.mu.Unlock()
    r.stopDevice()
    if len(r.pcmBuf) < 2 {
        return nil
@@ -203,23 +205,36 @@
// Close releases all audio resources.
func (r *Recorder) Close() {
    device := r.detachDevice()
    stopAndUninitDevice(device)
    r.mu.Lock()
    defer r.mu.Unlock()
    r.stopDevice()
    if r.ctx != nil {
        r.ctx.Free()
        r.ctx = nil
    }
}
func (r *Recorder) stopDevice() {
    if r.device != nil {
        r.device.Stop()
        r.device.Uninit()
func (r *Recorder) detachDevice() *malgo.Device {
    r.mu.Lock()
    defer r.mu.Unlock()
    device := r.device
        r.device = nil
    }
    r.isRecording = false
    return device
}
func stopAndUninitDevice(device *malgo.Device) {
    if device == nil {
        return
    }
    logger.Info("Stopping audio device")
    device.Stop()
    logger.Info("Audio device stopped")
    device.Uninit()
    logger.Info("Audio device uninitialized")
}
func (r *Recorder) onData(input []byte) {