11from __future__ import annotations
22
33import atexit
4+ import logging
45import os
56from typing import Generator , Iterator , TYPE_CHECKING
67from contextlib import contextmanager
1617 import miniaudio
1718 import samplerate
1819
20+ logger = logging .getLogger (__name__ )
21+
1922
2023class AudioOut :
2124 output_rate : float = 48000.0 # Hz
@@ -66,24 +69,22 @@ def __init__(
6669 # Diagnostics variables
6770 self ._underruns = 0
6871 self ._overruns = 0
69- self ._diag_enabled = bool (
70- os .environ .get ("GAMBATERM_AUDIO_CSV" )
71- or os .environ .get ("GAMBATERM_AUDIO_LOG" )
72- )
73- if self ._diag_enabled :
72+ self ._frame_num = 0
73+ self ._csv_enabled = bool (os .environ .get ("GAMBATERM_AUDIO_CSV" ))
74+ self ._diag_fill_min = 1.0
75+ self ._diag_ratio_min = self .nominal_sampling_ratio
76+ self ._diag_ratio_max = self .nominal_sampling_ratio
77+ if self ._csv_enabled :
7478 self ._diag_frames : list [dict ] = []
75- self ._diag_frame_num = 0
76- self ._diag_fill_min = 1.0
77- self ._diag_ratio_min = self .nominal_sampling_ratio
78- self ._diag_ratio_max = self .nominal_sampling_ratio
79- atexit .register (self ._dump_audio_stats )
79+ atexit .register (self ._dump_csv )
80+ atexit .register (self ._log_summary )
8081
8182 # Controller configuration
8283 self .correction_min = 1 - self .correction_clamp
8384 self .correction_max = 1 + self .correction_clamp
8485
85- # Batch variable-length emulator output to avoid starving the
86- # ring buffer as runFor() sometimes returns partial frames.
86+ # Batch the variable-length emulator audio output, avoids starving the ring buffer, runFor()
87+ # sometimes returns partial frames!
8788 self ._acc_buf : npt .NDArray [np .float32 ] = np .empty ((0 , 2 ), dtype = np .float32 )
8889
8990 # Controller state
@@ -107,16 +108,7 @@ def start(self) -> miniaudio.PlaybackDevice:
107108 device .start (stream )
108109 return device
109110
110- def _dump_audio_stats (self ) -> None :
111- if (audio_log := os .environ .get ("GAMBATERM_AUDIO_LOG" )):
112- with open (audio_log , "w" ) as fout :
113- fout .write (f"underruns={ self ._underruns } \n " )
114- fout .write (f"overruns={ self ._overruns } \n " )
115- fout .write (f"fill_min={ self ._diag_fill_min :.4f} \n " )
116- fout .write (f"ratio_min={ self ._diag_ratio_min :.8f} \n " )
117- fout .write (f"ratio_max={ self ._diag_ratio_max :.8f} \n " )
118- _rng = self ._diag_ratio_max - self ._diag_ratio_min
119- fout .write (f"ratio_range={ _rng :.8f} \n " )
111+ def _dump_csv (self ) -> None :
120112 if (diag_csv := os .environ .get ("GAMBATERM_AUDIO_CSV" )):
121113 with open (diag_csv , "w" ) as fout :
122114 fout .write ("frame,input,acc,proc,output,fill\n " )
@@ -126,6 +118,15 @@ def _dump_audio_stats(self) -> None:
126118 f"{ _df ['proc' ]} ,{ _df ['output' ]} ,{ _df ['fill' ]:.4f} \n "
127119 )
128120
121+ def _log_summary (self ) -> None :
122+ logger .debug ("Audio stats: underruns=%d overruns=%d "
123+ "fill_min=%.4f ratio_min=%.8f ratio_max=%.8f "
124+ "ratio_range=%.8f" ,
125+ self ._underruns , self ._overruns ,
126+ self ._diag_fill_min , self ._diag_ratio_min ,
127+ self ._diag_ratio_max ,
128+ self ._diag_ratio_max - self ._diag_ratio_min )
129+
129130 @property
130131 def fill_fraction (self ) -> float :
131132 # Ring buffer fill ratio (0-1.0)
@@ -162,45 +163,45 @@ def adapt_sample_rate(self) -> None:
162163 self ._diag_track_ratio ()
163164
164165 def _diag_record_skip (self , input_len : int , acc_len : int ) -> None :
165- if not self ._diag_enabled :
166- return
167166 fill = self .fill_fraction
168167 self ._diag_fill_min = min (self ._diag_fill_min , fill )
169- self ._diag_frames .append ({
170- "frame" : self ._diag_frame_num ,
171- "input" : input_len ,
172- "acc" : acc_len ,
173- "proc" : 0 ,
174- "output" : 0 ,
175- "fill" : fill ,
176- })
177- self ._diag_frame_num += 1
168+ frame = self ._frame_num
169+ self ._frame_num += 1
170+ logger .debug ("skip frame=%d input=%d acc=%d fill=%.4f" ,
171+ frame , input_len , acc_len , fill )
172+ if self ._csv_enabled :
173+ self ._diag_frames .append ({
174+ "frame" : frame ,
175+ "input" : input_len ,
176+ "acc" : acc_len ,
177+ "proc" : 0 ,
178+ "output" : 0 ,
179+ "fill" : fill ,
180+ })
178181
179182 def _diag_record_process (
180183 self , input_len : int , acc_len : int , output_len : int ,
181184 ) -> None :
182- if not self ._diag_enabled :
183- return
184185 fill = self .fill_fraction
185186 self ._diag_fill_min = min (self ._diag_fill_min , fill )
186- self ._diag_frames .append ({
187- "frame" : self ._diag_frame_num ,
188- "input" : input_len ,
189- "acc" : acc_len ,
190- "proc" : 1 ,
191- "output" : output_len ,
192- "fill" : fill ,
193- })
194- self ._diag_frame_num += 1
187+ frame = self ._frame_num
188+ self ._frame_num += 1
189+ logger .debug ("process frame=%d input=%d acc=%d output=%d fill=%.4f" ,
190+ frame , input_len , acc_len , output_len , fill )
191+ if self ._csv_enabled :
192+ self ._diag_frames .append ({
193+ "frame" : frame ,
194+ "input" : input_len ,
195+ "acc" : acc_len ,
196+ "proc" : 1 ,
197+ "output" : output_len ,
198+ "fill" : fill ,
199+ })
195200
196201 def _diag_track_fill (self ) -> None :
197- if not self ._diag_enabled :
198- return
199202 self ._diag_fill_min = min (self ._diag_fill_min , self .fill_fraction )
200203
201204 def _diag_track_ratio (self ) -> None :
202- if not self ._diag_enabled :
203- return
204205 self ._diag_ratio_min = min (self ._diag_ratio_min , self .sampling_ratio )
205206 self ._diag_ratio_max = max (self ._diag_ratio_max , self .sampling_ratio )
206207
@@ -246,6 +247,8 @@ def send(self, console: Console, audio: npt.NDArray[np.int16]) -> None:
246247 # Drop excess frames if we're overrun
247248 if frames > space :
248249 self ._overruns += 1
250+ logger .warning ("Audio overrun: dropping %d of %d frames (fill=%.2f)" ,
251+ frames - space , frames , self .fill_fraction )
249252 resampled = resampled [:space ]
250253 frames = space
251254
@@ -317,6 +320,8 @@ def _audio_stream(self) -> Generator[bytes, int, None]:
317320 # Log if we're underrunning
318321 if read_size < required_frames :
319322 self ._underruns += 1
323+ logger .warning ("Audio underrun: requested %d, got %d (fill=%.2f)" ,
324+ required_frames , read_size , self .fill_fraction )
320325
321326 # Send audio to output and get next required frames
322327 required_frames = yield result .tobytes ()
0 commit comments