22
33import logging
44import queue
5+ import sys
56import time
67
78import numpy as np
@@ -22,11 +23,25 @@ def __init__(self, sample_rate: int, channels: int, frame_ms: int, queue_seconds
2223 self ._stream : sd .RawInputStream | None = None
2324 self ._capture_rate = sample_rate
2425 self ._last_drop_log = 0.0
26+ self ._resample_source_rate = sample_rate
27+ self ._resample_buffer = np .empty (0 , dtype = np .float32 )
28+ self ._resample_position = 0.0
2529
2630 @property
2731 def active (self ) -> bool :
2832 return self ._stream is not None and self ._stream .active
2933
34+ @staticmethod
35+ def _preferred_input_device () -> tuple [int | None , object ]:
36+ if sys .platform == "win32" :
37+ for hostapi in sd .query_hostapis ():
38+ if str (hostapi ["name" ]).casefold () != "windows wasapi" :
39+ continue
40+ device_index = int (hostapi ["default_input_device" ])
41+ if device_index >= 0 :
42+ return device_index , sd .query_devices (device_index )
43+ return None , sd .query_devices (kind = "input" )
44+
3045 def _callback (self , indata : bytes , frames : int , time_info : object , status : sd .CallbackFlags ) -> None :
3146 del frames , time_info
3247 frame = (bytes (indata ), self ._capture_rate )
@@ -51,23 +66,34 @@ def _callback(self, indata: bytes, frames: int, time_info: object, status: sd.Ca
5166 def start (self ) -> None :
5267 self .stop ()
5368 self .clear ()
54- device = sd . query_devices ( kind = "input" )
69+ device_index , device = self . _preferred_input_device ( )
5570 native_rate = int (round (float (device ["default_samplerate" ])))
56- try :
57- sd .check_input_settings (channels = self .channels , dtype = "int16" , samplerate = self .sample_rate )
58- self ._capture_rate = self .sample_rate
59- except sd .PortAudioError :
60- self ._capture_rate = native_rate
61- LOGGER .info ("microphone does not accept %s Hz; capturing at %s Hz" , self .sample_rate , native_rate )
71+ self ._capture_rate = native_rate
72+ sd .check_input_settings (
73+ device = device_index ,
74+ channels = self .channels ,
75+ dtype = "int16" ,
76+ samplerate = self ._capture_rate ,
77+ )
6278 blocksize = max (1 , round (self ._capture_rate * self .frame_ms / 1000 ))
6379 self ._stream = sd .RawInputStream (
80+ device = device_index ,
6481 samplerate = self ._capture_rate ,
6582 blocksize = blocksize ,
6683 channels = self .channels ,
6784 dtype = "int16" ,
6885 callback = self ._callback ,
6986 )
7087 self ._stream .start ()
88+ hostapi = sd .query_hostapis (int (device ["hostapi" ]))
89+ LOGGER .info (
90+ "microphone opened device=%s name=%r hostapi=%s capture_rate=%s output_rate=%s" ,
91+ device_index if device_index is not None else "<default>" ,
92+ device ["name" ],
93+ hostapi ["name" ],
94+ self ._capture_rate ,
95+ self .sample_rate ,
96+ )
7197
7298 def stop (self ) -> None :
7399 stream , self ._stream = self ._stream , None
@@ -82,20 +108,40 @@ def clear(self) -> None:
82108 try :
83109 self ._queue .get_nowait ()
84110 except queue .Empty :
85- return
111+ break
112+ self ._resample_source_rate = self .sample_rate
113+ self ._resample_buffer = np .empty (0 , dtype = np .float32 )
114+ self ._resample_position = 0.0
86115
87- def get_frame (self , timeout : float = 0.1 ) -> bytes | None :
88- try :
89- pcm , source_rate = self ._queue .get (timeout = timeout )
90- except queue .Empty :
91- return None
116+ def _resample (self , pcm : bytes , source_rate : int ) -> bytes :
92117 if source_rate == self .sample_rate :
93118 return pcm
94119 samples = np .frombuffer (pcm , dtype = "<i2" ).astype (np .float32 )
120+ if source_rate != self ._resample_source_rate :
121+ self ._resample_source_rate = source_rate
122+ self ._resample_buffer = np .empty (0 , dtype = np .float32 )
123+ self ._resample_position = 0.0
124+ if self ._resample_buffer .size :
125+ samples = np .concatenate ((self ._resample_buffer , samples ))
95126 if samples .size < 2 :
96- return pcm
97- output_size = max (1 , round (samples .size * self .sample_rate / source_rate ))
98- positions = np .linspace (0 , samples .size - 1 , output_size )
127+ self ._resample_buffer = samples
128+ return b""
129+
130+ step = source_rate / float (self .sample_rate )
131+ positions = np .arange (self ._resample_position , samples .size - 1 , step , dtype = np .float64 )
132+ if positions .size == 0 :
133+ self ._resample_buffer = samples
134+ return b""
99135 resampled = np .interp (positions , np .arange (samples .size ), samples )
136+ next_position = float (positions [- 1 ] + step )
137+ consumed = min (int (np .floor (next_position )), samples .size )
138+ self ._resample_buffer = samples [consumed :]
139+ self ._resample_position = next_position - consumed
100140 return np .clip (resampled , - 32768 , 32767 ).astype ("<i2" ).tobytes ()
101141
142+ def get_frame (self , timeout : float = 0.1 ) -> bytes | None :
143+ try :
144+ pcm , source_rate = self ._queue .get (timeout = timeout )
145+ except queue .Empty :
146+ return None
147+ return self ._resample (pcm , source_rate )
0 commit comments