| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- """
- Audio Recorder — Capture audio from sound card input.
- """
- import sounddevice as sd
- import numpy as np
- import soundfile as sf
- from typing import Optional, Callable
- import threading
- from .audio_device import DeviceManager
- class AudioRecorder:
- """
- Captures audio from the selected input device.
- Supports continuous ring-buffering for UI updates and saving to file.
- """
- def __init__(self, device_manager: DeviceManager):
- self.device_manager = device_manager
-
- self.stream: Optional[sd.InputStream] = None
- self.is_recording: bool = False
-
- # Buffer for capturing the full recording
- self.recorded_data: list[np.ndarray] = []
-
- # Callbacks
- self.on_audio_data: Optional[Callable[[np.ndarray], None]] = None
-
- self.vu_levels = (-100.0, -100.0)
- def start(self):
- """Start capturing audio."""
- if self.is_recording:
- return
-
- self.recorded_data = []
-
- kwargs = self.device_manager.get_sd_input_kwargs()
-
- self.stream = sd.InputStream(
- callback=self._audio_callback,
- **kwargs
- )
-
- self.is_recording = True
- self.stream.start()
- def stop(self) -> np.ndarray:
- """Stop capturing and return the recorded data."""
- if self.is_recording and self.stream:
- self.stream.stop()
- self.stream.close()
- self.stream = None
- self.is_recording = False
-
- if not self.recorded_data:
- channels = self.device_manager.channels
- return np.zeros((0, channels), dtype=np.float32)
-
- # Concatenate all blocks
- full_data = np.concatenate(self.recorded_data, axis=0)
- return full_data
- def save_to_wav(self, file_path: str):
- """Save the recorded data to a WAV file."""
- data = self.stop()
- if len(data) > 0:
- sf.write(
- file_path,
- data,
- self.device_manager.sample_rate,
- subtype='FLOAT'
- )
- return True
- return False
- def _audio_callback(self, indata: np.ndarray, frames: int, time, status: sd.CallbackFlags):
- """sounddevice callback for incoming audio."""
- if status:
- print(f"Record status: {status}")
-
- # Make a copy of the input data
- chunk = indata.copy()
-
- # Store for full recording
- self.recorded_data.append(chunk)
-
- # Update VU
- self._update_vu_levels(chunk)
-
- # Notify UI/analyzers (e.g. for real-time spectrum)
- if self.on_audio_data:
- self.on_audio_data(chunk)
- def _update_vu_levels(self, chunk: np.ndarray):
- """Calculate RMS levels for VU meters."""
- if chunk.size == 0:
- return
-
- rms = np.sqrt(np.mean(chunk**2, axis=0))
- db = 20 * np.log10(np.maximum(rms, 1e-5))
-
- if chunk.shape[1] >= 2:
- self.vu_levels = (float(db[0]), float(db[1]))
- else:
- self.vu_levels = (float(db[0]), float(db[0]))
|