From 015f1089ea875a153dd2a18ef42c25145d4868f5 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 11 Jul 2026 22:28:32 +0800
Subject: [PATCH] freeze(mas): snapshot tested 2.2.3 build 20260711.1807
---
C1.source/privatevoice.src/internal/audio/recorder.go | 254 ++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 231 insertions(+), 23 deletions(-)
diff --git a/C1.source/privatevoice.src/internal/audio/recorder.go b/C1.source/privatevoice.src/internal/audio/recorder.go
index 9cf8b92..44b3a2f 100755
--- a/C1.source/privatevoice.src/internal/audio/recorder.go
+++ b/C1.source/privatevoice.src/internal/audio/recorder.go
@@ -1,6 +1,8 @@
package audio
import (
+ "errors"
+ "fmt"
"math"
"sync"
"time"
@@ -18,9 +20,12 @@
maxPCMBufferBytes = sampleRate * channels * (bitsPerSample / 8) * maxRecordingSeconds
MaxRecordingDuration = time.Duration(maxRecordingSeconds) * time.Second
maxPendingDeviceStops = 2
+ deviceStartWatchDelay = 2 * time.Second
)
var pendingDeviceStops = make(chan struct{}, maxPendingDeviceStops)
+
+var ErrMicrophonePermission = errors.New("microphone permission not granted")
// InputDevice represents an audio input device.
type InputDevice struct {
@@ -28,12 +33,41 @@
IsDefault bool `json:"isDefault"`
}
+type recorderAudioContext interface {
+ devices(malgo.DeviceType) ([]malgo.DeviceInfo, error)
+ initDevice(malgo.DeviceConfig, malgo.DeviceCallbacks) (recorderDevice, error)
+ free()
+}
+
+type malgoRecorderContext struct {
+ allocated *malgo.AllocatedContext
+}
+
+func (c *malgoRecorderContext) devices(deviceType malgo.DeviceType) ([]malgo.DeviceInfo, error) {
+ return c.allocated.Context.Devices(deviceType)
+}
+
+func (c *malgoRecorderContext) initDevice(config malgo.DeviceConfig, callbacks malgo.DeviceCallbacks) (recorderDevice, error) {
+ return malgo.InitDevice(c.allocated.Context, config, callbacks)
+}
+
+func (c *malgoRecorderContext) free() {
+ c.allocated.Free()
+}
+
+type recorderDevice interface {
+ Start() error
+ Stop() error
+ Uninit()
+}
+
// Recorder captures audio from the default input device using malgo (miniaudio).
type Recorder struct {
- mu sync.Mutex
+ mu sync.Mutex
+ lifecycleMu sync.Mutex
- ctx *malgo.AllocatedContext
- device *malgo.Device
+ ctx recorderAudioContext
+ device recorderDevice
// PCM buffer (16-bit signed, little-endian)
pcmBuf []byte
@@ -43,7 +77,11 @@
// State
isRecording bool
+ startRequested bool
+ isStarting bool
+ stopAfterStart bool
bufferLimitHit bool
+ closing bool
maxVolume float64
currentVolume float64
volumeCallback func(float64)
@@ -58,7 +96,7 @@
logger.Error("Failed to init malgo context: %v", err)
return &Recorder{}
}
- return &Recorder{ctx: ctx}
+ return &Recorder{ctx: &malgoRecorderContext{allocated: ctx}}
}
// OnVolume registers a callback for real-time volume updates.
@@ -77,10 +115,14 @@
// ListInputDevices returns all available audio capture devices.
func (r *Recorder) ListInputDevices() []InputDevice {
- if r.ctx == nil {
+ r.lifecycleMu.Lock()
+ defer r.lifecycleMu.Unlock()
+
+ ctx := r.ctx
+ if ctx == nil {
return nil
}
- infos, err := r.ctx.Context.Devices(malgo.Capture)
+ infos, err := ctx.devices(malgo.Capture)
if err != nil {
logger.Error("Failed to list devices: %v", err)
return nil
@@ -105,10 +147,16 @@
// Start begins recording from the preferred (or default) capture device.
func (r *Recorder) Start() error {
- r.mu.Lock()
- defer r.mu.Unlock()
+ r.lifecycleMu.Lock()
+ defer r.lifecycleMu.Unlock()
- if r.isRecording {
+ r.mu.Lock()
+ if r.closing {
+ r.mu.Unlock()
+ return fmt.Errorf("audio recorder is closed")
+ }
+ if r.isRecording || r.startRequested || r.isStarting {
+ r.mu.Unlock()
return nil
}
@@ -116,6 +164,47 @@
r.bufferLimitHit = false
r.maxVolume = 0
r.currentVolume = 0
+ r.startRequested = true
+ r.stopAfterStart = false
+ ctx := r.ctx
+ preferredDevice := r.preferredDevice
+ r.mu.Unlock()
+ logger.Info("Audio recorder start requested sample_rate=%d channels=%d preferred_device=%q", sampleRate, channels, preferredDevice)
+
+ auth := ensureMicrophoneAuthorization()
+ if !auth.granted {
+ r.mu.Lock()
+ r.startRequested = false
+ r.stopAfterStart = false
+ r.isRecording = false
+ r.mu.Unlock()
+ logger.Error("Microphone permission not granted; audio recorder start blocked state=%s", auth.state)
+ return fmt.Errorf("%w: %s", ErrMicrophonePermission, auth.state)
+ }
+ logger.Info("Microphone permission ready state=%s", auth.state)
+
+ r.mu.Lock()
+ stopBeforeAuthComplete := r.stopAfterStart || r.closing
+ startWasClosed := r.closing
+ if stopBeforeAuthComplete {
+ r.startRequested = false
+ r.stopAfterStart = false
+ r.isRecording = false
+ r.mu.Unlock()
+ if startWasClosed {
+ return fmt.Errorf("audio recorder closed during start")
+ }
+ return nil
+ }
+ r.mu.Unlock()
+
+ if ctx == nil {
+ r.mu.Lock()
+ r.startRequested = false
+ r.stopAfterStart = false
+ r.mu.Unlock()
+ return fmt.Errorf("audio context is not initialized")
+ }
deviceConfig := malgo.DefaultDeviceConfig(malgo.Capture)
deviceConfig.Capture.Format = malgo.FormatS16
@@ -124,11 +213,11 @@
deviceConfig.PeriodSizeInMilliseconds = 50
// Use preferred device if set
- if r.preferredDevice != "" {
- infos, err := r.ctx.Context.Devices(malgo.Capture)
+ if preferredDevice != "" {
+ infos, err := ctx.devices(malgo.Capture)
if err == nil {
for _, info := range infos {
- if info.Name() == r.preferredDevice {
+ if info.Name() == preferredDevice {
deviceConfig.Capture.DeviceID = info.ID.Pointer()
break
}
@@ -142,18 +231,98 @@
},
}
- device, err := malgo.InitDevice(r.ctx.Context, deviceConfig, callbacks)
+ device, err := ctx.initDevice(deviceConfig, callbacks)
if err != nil {
+ r.mu.Lock()
+ r.startRequested = false
+ r.stopAfterStart = false
+ r.mu.Unlock()
return err
}
+ logger.Info("Audio device initialized; starting native device")
- if err := device.Start(); err != nil {
+ r.mu.Lock()
+ stopBeforeStart := r.stopAfterStart || r.closing
+ startWasClosed = r.closing
+ if r.isRecording || stopBeforeStart {
+ r.startRequested = false
+ r.stopAfterStart = false
+ r.isRecording = false
+ r.mu.Unlock()
device.Uninit()
- return err
+ if startWasClosed {
+ return fmt.Errorf("audio recorder closed during start")
+ }
+ return nil
}
-
r.device = device
r.isRecording = true
+ r.startRequested = false
+ r.isStarting = true
+ r.stopAfterStart = false
+ r.mu.Unlock()
+
+ deviceStartBegin := time.Now()
+ watchDone := make(chan struct{})
+ go func() {
+ select {
+ case <-watchDone:
+ case <-time.After(deviceStartWatchDelay):
+ logger.Error("Native audio device start still pending after %dms", deviceStartWatchDelay.Milliseconds())
+ }
+ }()
+ if err := device.Start(); err != nil {
+ close(watchDone)
+ r.mu.Lock()
+ stillCurrent := r.device == device
+ if stillCurrent {
+ r.device = nil
+ r.isRecording = false
+ r.startRequested = false
+ r.isStarting = false
+ r.stopAfterStart = false
+ }
+ r.mu.Unlock()
+ if stillCurrent {
+ device.Uninit()
+ }
+ return err
+ }
+ close(watchDone)
+ logger.Info("Native audio device start returned result=success start_ms=%d", time.Since(deviceStartBegin).Milliseconds())
+
+ r.mu.Lock()
+ stillCurrent := r.device == device
+ shouldStop := stillCurrent && (r.stopAfterStart || r.closing || !r.isRecording)
+ startWasClosed = stillCurrent && r.closing
+ if !stillCurrent && r.device == device {
+ r.device = nil
+ r.isRecording = false
+ r.isStarting = false
+ r.stopAfterStart = false
+ }
+ if stillCurrent {
+ r.startRequested = false
+ r.isStarting = false
+ }
+ if shouldStop {
+ r.device = nil
+ r.isRecording = false
+ r.stopAfterStart = false
+ }
+ r.mu.Unlock()
+ if !stillCurrent {
+ stopAndUninitDevice(device)
+ return fmt.Errorf("audio recorder closed during start")
+ }
+ if shouldStop {
+ stopAndUninitDevice(device)
+ if startWasClosed {
+ return fmt.Errorf("audio recorder closed during start")
+ }
+ return nil
+ }
+
logger.Info("Recording started (16kHz/16-bit/mono)")
return nil
}
@@ -208,10 +377,14 @@
// GetDeviceName returns the name of the current capture device.
func (r *Recorder) GetDeviceName() string {
- if r.ctx == nil {
+ r.lifecycleMu.Lock()
+ defer r.lifecycleMu.Unlock()
+
+ ctx := r.ctx
+ if ctx == nil {
return "Default"
}
- devices, err := r.ctx.Context.Devices(malgo.Capture)
+ devices, err := ctx.devices(malgo.Capture)
if err != nil || len(devices) == 0 {
return "Default"
}
@@ -220,32 +393,67 @@
// Close releases all audio resources.
func (r *Recorder) Close() {
+ r.mu.Lock()
+ r.closing = true
+ r.mu.Unlock()
+
+ r.lifecycleMu.Lock()
+ defer r.lifecycleMu.Unlock()
+
device := r.detachDevice()
stopAndUninitDevice(device)
r.mu.Lock()
defer r.mu.Unlock()
if r.ctx != nil {
- r.ctx.Free()
+ r.ctx.free()
r.ctx = nil
}
}
-func (r *Recorder) detachDevice() *malgo.Device {
+func (r *Recorder) detachDevice() recorderDevice {
r.mu.Lock()
defer r.mu.Unlock()
device := r.device
+ if r.startRequested && !r.isStarting {
+ logger.Info("Recording stop requested before audio device was published; deferring startup cleanup")
+ r.isRecording = false
+ r.stopAfterStart = true
+ return nil
+ }
+ if r.isStarting {
+ logger.Info("Recording stop requested while audio device start is still pending; deferring native device cleanup")
+ r.isRecording = false
+ r.stopAfterStart = true
+ return nil
+ }
r.device = nil
r.isRecording = false
return device
}
-func (r *Recorder) detachDeviceAndPCM() (*malgo.Device, []byte) {
+func (r *Recorder) detachDeviceAndPCM() (recorderDevice, []byte) {
r.mu.Lock()
defer r.mu.Unlock()
device := r.device
+ if r.startRequested && !r.isStarting {
+ logger.Info("Recording samples requested before audio device was published; snapshotting buffered PCM and deferring startup cleanup")
+ r.isRecording = false
+ r.stopAfterStart = true
+ pcm := append([]byte(nil), r.pcmBuf...)
+ r.pcmBuf = nil
+ return nil, pcm
+ }
+ if r.isStarting {
+ logger.Info("Recording samples requested while audio device start is still pending; snapshotting buffered PCM and deferring native device cleanup")
+ r.isRecording = false
+ r.stopAfterStart = true
+ pcm := append([]byte(nil), r.pcmBuf...)
+ r.pcmBuf = nil
+ return nil, pcm
+ }
r.device = nil
r.isRecording = false
pcm := append([]byte(nil), r.pcmBuf...)
@@ -253,7 +461,7 @@
return device, pcm
}
-func stopAndUninitDeviceAsync(device *malgo.Device) {
+func stopAndUninitDeviceAsync(device recorderDevice) {
if device == nil {
return
}
@@ -281,7 +489,7 @@
}()
}
-func stopAndUninitDevice(device *malgo.Device) {
+func stopAndUninitDevice(device recorderDevice) {
if device == nil {
return
}
--
Gitblit v1.9.3