1- # core/audio/manager.py
1+ from __future__ import annotations
2+
23from typing import Optional
3- from pathlib import Path
4- import tempfile
54import wave
5+
66from PySide6 .QtCore import QObject , Signal , Slot
7- from .recording import RecordingThread
7+
8+ from core .audio .recording import RecordingThread
9+ from core .temp_file_manager import temp_file_manager
10+ from core .logging_config import get_logger
11+ from core .exceptions import AudioSaveError
12+
13+ logger = get_logger (__name__ )
14+
815
916class AudioManager (QObject ):
1017 recording_started = Signal ()
1118 recording_stopped = Signal ()
12- audio_ready = Signal (str ) # file path
19+ audio_ready = Signal (str )
1320 audio_error = Signal (str )
1421
1522 def __init__ (self , samplerate : int = 44_100 , channels : int = 1 , dtype : str = "int16" ):
@@ -18,55 +25,89 @@ def __init__(self, samplerate: int = 44_100, channels: int = 1, dtype: str = "in
1825 self .channels = channels
1926 self .dtype = dtype
2027 self ._recording_thread : Optional [RecordingThread ] = None
28+ self ._current_temp_file : Optional [str ] = None
2129
2230 def start_recording (self ) -> bool :
23- """Start audio recording."""
2431 if self ._recording_thread and self ._recording_thread .isRunning ():
32+ logger .warning ("Attempted to start recording while already recording" )
33+ return False
34+
35+ try :
36+ self ._recording_thread = RecordingThread (
37+ self .samplerate , self .channels , self .dtype
38+ )
39+ self ._recording_thread .recording_error .connect (self ._on_recording_error )
40+ self ._recording_thread .recording_finished .connect (self ._on_recording_finished )
41+ self ._recording_thread .start ()
42+ self .recording_started .emit ()
43+ logger .info ("Recording started" )
44+ return True
45+ except Exception as e :
46+ logger .exception ("Failed to start recording" )
47+ self .audio_error .emit (f"Failed to start recording: { e } " )
2548 return False
26-
27- self ._recording_thread = RecordingThread (
28- self .samplerate , self .channels , self .dtype
29- )
30- self ._recording_thread .recording_error .connect (self .audio_error )
31- self ._recording_thread .recording_finished .connect (self ._on_recording_finished )
32- self ._recording_thread .start ()
33- self .recording_started .emit ()
34- return True
3549
3650 def stop_recording (self ) -> None :
37- """Stop audio recording."""
3851 if self ._recording_thread and self ._recording_thread .isRunning ():
3952 self ._recording_thread .stop ()
53+ logger .info ("Recording stop requested" )
4054 self .recording_stopped .emit ()
4155
56+ @Slot (str )
57+ def _on_recording_error (self , error : str ) -> None :
58+ logger .error (f"Recording error: { error } " )
59+ self .audio_error .emit (error )
60+
4261 @Slot ()
4362 def _on_recording_finished (self ) -> None :
44- """Handle recording completion and save to file."""
4563 try :
46- audio_file = self ._save_recording_to_file ()
64+ if self ._recording_thread is None :
65+ raise AudioSaveError ("No recording thread available" )
66+
67+ buffer_contents = self ._recording_thread .get_buffer_contents ()
68+ if not buffer_contents :
69+ raise AudioSaveError ("No audio data recorded" )
70+
71+ audio_file = self ._save_recording_to_file (buffer_contents )
72+ self ._current_temp_file = str (audio_file )
73+ logger .info (f"Audio saved to: { audio_file } " )
4774 self .audio_ready .emit (str (audio_file ))
75+
76+ except AudioSaveError as e :
77+ logger .error (f"Audio save error: { e } " )
78+ self .audio_error .emit (str (e ))
4879 except Exception as e :
80+ logger .exception ("Unexpected error saving audio" )
4981 self .audio_error .emit (f"Failed to save audio: { e } " )
5082
51- def _save_recording_to_file (self ) -> Path :
52- """Save recorded audio to temporary WAV file."""
53- with tempfile .NamedTemporaryFile (suffix = ".wav" , delete = False ) as tf :
54- path = Path (tf .name )
55-
56- with wave .open (str (path ), "wb" ) as wf :
57- wf .setnchannels (self .channels )
58- wf .setsampwidth (self ._sample_width ())
59- wf .setframerate (self .samplerate )
60- while not self ._recording_thread .buffer .empty ():
61- wf .writeframes (self ._recording_thread .buffer .get ().tobytes ())
83+ def _save_recording_to_file (self , buffer_contents : list ):
84+ path = temp_file_manager .create_temp_wav ()
6285
63- return path
86+ try :
87+ with wave .open (str (path ), "wb" ) as wf :
88+ wf .setnchannels (self .channels )
89+ wf .setsampwidth (self ._sample_width ())
90+ wf .setframerate (self .samplerate )
91+ for chunk in buffer_contents :
92+ wf .writeframes (chunk .tobytes ())
93+ return path
94+ except Exception as e :
95+ temp_file_manager .release (path )
96+ raise AudioSaveError (f"Failed to write WAV file: { e } " ) from e
6497
6598 def _sample_width (self ) -> int :
6699 return {"int16" : 2 , "int32" : 4 , "float32" : 4 }.get (self .dtype , 2 )
67100
68101 def cleanup (self ) -> None :
69- """Clean up audio resources."""
70102 if self ._recording_thread and self ._recording_thread .isRunning ():
71103 self ._recording_thread .stop ()
72- self ._recording_thread .wait ()
104+ self ._recording_thread .wait (5000 )
105+ if self ._recording_thread .isRunning ():
106+ logger .warning ("Recording thread did not stop in time" )
107+ self ._recording_thread .terminate ()
108+
109+ if self ._current_temp_file :
110+ temp_file_manager .release (self ._current_temp_file )
111+ self ._current_temp_file = None
112+
113+ logger .debug ("AudioManager cleanup complete" )
0 commit comments