Ariver
2026-07-11 015f1089ea875a153dd2a18ef42c25145d4868f5
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
   }