@@ -39,7 +39,7 @@ def __init__(
3939 self ,
4040 sample_rate : int = 16000 ,
4141 wave_type : WaveType = "sine" ,
42- block_duration : float = 0.03 ,
42+ block_duration : float = 0.01 ,
4343 attack : float = 0.01 ,
4444 release : float = 0.03 ,
4545 glide : float = 0.02 ,
@@ -50,13 +50,29 @@ def __init__(
5050 Args:
5151 sample_rate (int): Audio sample rate in Hz (default: 16000).
5252 wave_type (WaveType): Initial waveform type (default: "sine").
53- block_duration (float): Duration of each audio block in seconds (default: 0.03 ).
53+ block_duration (float): Duration of each audio block in seconds (default: 0.01 ).
5454 attack (float): Attack time for amplitude envelope in seconds (default: 0.01).
5555 release (float): Release time for amplitude envelope in seconds (default: 0.03).
5656 glide (float): Frequency glide time (portamento) in seconds (default: 0.02).
57- speaker (Speaker, optional): Pre-configured Speaker instance. If None, a new Speaker
58- will be created with default settings (auto-detect device, FLOAT_LE format).
59- WaveGenerator will manage the speaker's lifecycle (calling start/stop).
57+ speaker (Speaker, optional): Pre-configured Speaker instance. If None, WaveGenerator
58+ will create an internal Speaker optimized for real-time synthesis with:
59+ - periodsize aligned to block_duration (eliminates buffer mismatch)
60+ - queue_maxsize=8 (low latency: ~80ms max buffer)
61+ - format=FLOAT_LE, channels=1
62+
63+ If providing an external Speaker, ensure:
64+ - sample_rate matches WaveGenerator's sample_rate
65+ - periodsize = int(sample_rate × block_duration) for optimal alignment
66+ - Speaker is started/stopped manually (WaveGenerator won't manage its lifecycle)
67+
68+ Example external Speaker configuration:
69+ speaker = Speaker(
70+ device="plughw:CARD=UH34",
71+ sample_rate=16000,
72+ format="FLOAT_LE",
73+ periodsize=160, # 16000 × 0.01 = 160 frames
74+ queue_maxsize=8
75+ )
6076
6177 Raises:
6278 SpeakerException: If no USB speaker is found or device is busy.
@@ -90,18 +106,51 @@ def __init__(
90106 if speaker is not None :
91107 # Use externally provided Speaker instance
92108 self ._speaker = speaker
93- logger .info ("Using externally provided Speaker instance" )
109+ self ._owns_speaker = False
110+
111+ # Validate external speaker configuration
112+ if self ._speaker .sample_rate != sample_rate :
113+ logger .warning (
114+ f"External Speaker sample_rate ({ self ._speaker .sample_rate } Hz) differs from "
115+ f"WaveGenerator sample_rate ({ sample_rate } Hz). This may cause issues."
116+ )
117+
118+ # Check if periodsize is aligned with block_duration
119+ expected_periodsize = int (sample_rate * block_duration )
120+ if self ._speaker ._periodsize is not None and self ._speaker ._periodsize != expected_periodsize :
121+ logger .warning (
122+ f"External Speaker periodsize ({ self ._speaker ._periodsize } frames) does not match "
123+ f"WaveGenerator block size ({ expected_periodsize } frames @ { sample_rate } Hz). "
124+ f"This may cause audio glitches. Consider using periodsize={ expected_periodsize } "
125+ f"or omitting the speaker parameter to use auto-configured internal Speaker."
126+ )
127+ elif self ._speaker ._periodsize is None :
128+ logger .warning (
129+ f"External Speaker has periodsize=None (hardware default). For optimal real-time "
130+ f"synthesis, consider setting periodsize={ expected_periodsize } frames to match "
131+ f"WaveGenerator block size."
132+ )
133+
134+ logger .info ("Using externally provided Speaker instance (not managed by WaveGenerator)" )
94135 else :
95- # Create internal Speaker instance with default settings
136+ # Create internal Speaker instance optimized for real-time synthesis
137+ # WaveGenerator requires low-latency configuration:
138+ # - periodsize matches block_duration for aligned I/O
139+ # - queue_maxsize reduced for responsive audio
140+ block_size_frames = int (sample_rate * block_duration )
96141 self ._speaker = Speaker (
97- device = None , # Auto-detect first available USB speaker
142+ device = Speaker . USB_SPEAKER_1 , # Auto-detect first available USB speaker
98143 sample_rate = sample_rate ,
99144 channels = 1 ,
100145 format = "FLOAT_LE" ,
146+ periodsize = block_size_frames , # Align with generation block
147+ queue_maxsize = 8 , # Extreme low latency: 8 blocks = 80ms @ 10ms/block
101148 )
149+ self ._owns_speaker = True
102150 logger .info (
103- "Created internal Speaker: device=auto-detect, sample_rate=%d, format=FLOAT_LE" ,
151+ "Created internal Speaker: device=auto-detect, sample_rate=%d, format=FLOAT_LE, periodsize=%d frames " ,
104152 sample_rate ,
153+ block_size_frames ,
105154 )
106155
107156 # Producer thread control
@@ -119,22 +168,29 @@ def __init__(
119168 def start (self ):
120169 """Start the wave generator and audio output.
121170
122- This starts the speaker device and launches the producer thread that
123- continuously generates and streams audio blocks.
171+ This starts the speaker device (if internally owned) and launches the producer thread
172+ that continuously generates and streams audio blocks.
124173 """
125174 if self ._running .is_set ():
126175 logger .warning ("WaveGenerator is already running" )
127176 return
128177
129178 logger .info ("Starting WaveGenerator..." )
130- self ._speaker .start ()
131179
132- # Set hardware speaker volume to maximum (100%)
133- try :
134- self ._speaker .set_volume (100 )
135- logger .info ("Speaker hardware volume set to 100%" )
136- except Exception as e :
137- logger .warning (f"Could not set speaker volume: { e } " )
180+ if self ._owns_speaker :
181+ self ._speaker .start ()
182+
183+ # Set hardware speaker volume to maximum (100%)
184+ try :
185+ self ._speaker .set_volume (100 )
186+ logger .info ("Speaker hardware volume set to 100%" )
187+ except Exception as e :
188+ logger .warning (f"Could not set speaker volume: { e } " )
189+ else :
190+ logger .info ("Using external Speaker (lifecycle not managed by WaveGenerator)" )
191+ # Verify external speaker is started
192+ if not self ._speaker ._is_reproducing .is_set ():
193+ logger .warning ("External Speaker is not started! Call speaker.start() before starting WaveGenerator, or audio playback will fail." )
138194
139195 self ._running .set ()
140196
@@ -146,7 +202,7 @@ def start(self):
146202 def stop (self ):
147203 """Stop the wave generator and audio output.
148204
149- This stops the producer thread and closes the speaker device.
205+ This stops the producer thread and closes the speaker device (if internally owned) .
150206 """
151207 if not self ._running .is_set ():
152208 logger .warning ("WaveGenerator is not running" )
@@ -161,8 +217,12 @@ def stop(self):
161217 logger .warning ("Producer thread did not terminate in time" )
162218 self ._producer_thread = None
163219
164- self ._speaker .stop ()
165- logger .info ("WaveGenerator stopped" )
220+ # Only stop speaker if we own it (internal instance)
221+ if self ._owns_speaker :
222+ self ._speaker .stop ()
223+ logger .info ("WaveGenerator stopped (internal Speaker closed)" )
224+ else :
225+ logger .info ("WaveGenerator stopped (external Speaker not closed)" )
166226
167227 def set_frequency (self , frequency : float ):
168228 """Set the target output frequency.
@@ -265,11 +325,13 @@ def _producer_loop(self):
265325 """Main producer loop running in background thread.
266326
267327 Continuously generates audio blocks at a steady cadence and streams
268- them to the speaker device.
328+ them to the speaker device. Uses adaptive generation: when queue is low,
329+ generates extra blocks to prevent underruns and glitches.
269330 """
270331 logger .debug ("Producer loop started" )
271332 next_time = time .perf_counter ()
272333 block_count = 0
334+ emergency_refill_threshold = 2 # Generate extra blocks if queue below this (~20ms @ 10ms/block)
273335
274336 while self ._running .is_set ():
275337 next_time += self .block_duration
@@ -285,10 +347,20 @@ def _producer_loop(self):
285347 if block_count % 100 == 0 or (block_count < 5 ):
286348 logger .debug (f"Producer: block={ block_count } , freq={ target_freq :.1f} Hz, amp={ target_amp :.3f} " )
287349
288- # Generate audio block
350+ # Check queue depth and generate extra blocks if needed
351+ queue_depth = self ._speaker ._playing_queue .qsize ()
352+ blocks_to_generate = 1
353+ if queue_depth < emergency_refill_threshold :
354+ # Emergency: generate 2-3 blocks at once to quickly refill
355+ blocks_to_generate = min (3 , emergency_refill_threshold - queue_depth )
356+ if blocks_to_generate > 1 :
357+ logger .debug (f"Emergency refill: queue={ queue_depth } , generating { blocks_to_generate } blocks" )
358+
359+ # Generate audio block(s)
289360 try :
290- audio_block = self ._generate_block (target_freq , target_amp , wave_type )
291- self ._speaker .play (audio_block , block_on_queue = False )
361+ for _ in range (blocks_to_generate ):
362+ audio_block = self ._generate_block (target_freq , target_amp , wave_type )
363+ self ._speaker .play (audio_block , block_on_queue = False )
292364 except Exception as e :
293365 logger .error (f"Error generating audio block: { e } " )
294366
0 commit comments