|
4 | 4 | import os |
5 | 5 | import math |
6 | 6 | import wave |
| 7 | +import signal |
| 8 | +from multiprocessing import Process, Value, Event |
| 9 | +from multiprocessing.shared_memory import SharedMemory |
7 | 10 |
|
8 | 11 | import numpy as np |
9 | 12 | from numba import jit |
@@ -195,3 +198,167 @@ def get_audio_properties(input_path: str) -> Tuple[int, int]: |
195 | 198 | rate = audio_stream.base_rate |
196 | 199 | container.close() |
197 | 200 | return channels, rate |
| 201 | + |
| 202 | +class AudioIoProcess(Process): |
| 203 | + def __init__(self, |
| 204 | + input_device, |
| 205 | + output_device, |
| 206 | + input_audio_block_size: int, |
| 207 | + sample_rate: int, |
| 208 | + channel_num: int = 2, |
| 209 | + is_device_combined: bool = True, |
| 210 | + is_input_wasapi_exclusive: bool = False, |
| 211 | + is_output_wasapi_exclusive: bool = False |
| 212 | + ): |
| 213 | + super().__init__() |
| 214 | + self.in_dev = input_device |
| 215 | + self.out_dev = output_device |
| 216 | + self.block_size: int = input_audio_block_size |
| 217 | + self.buf_size: int = self.block_size << 1 # 双缓冲 |
| 218 | + self.sample_rate: int = sample_rate |
| 219 | + self.channels: int = channel_num |
| 220 | + self.is_device_combined: bool = is_device_combined |
| 221 | + self.is_input_wasapi_exclusive: bool = is_input_wasapi_exclusive |
| 222 | + self.is_output_wasapi_exclusive: bool = is_output_wasapi_exclusive |
| 223 | + |
| 224 | + self.__rec_ptr = 0 |
| 225 | + self.in_ptr = Value('i', 0) # 当收满一个block时由本进程设置 |
| 226 | + self.out_ptr = Value('i', 0) # 由主进程设置,指示下一次预期写入位置 |
| 227 | + self.play_ptr = Value('i', 0) # 由本进程设置,指示当前音频已经播放到哪里 |
| 228 | + self.in_evt = Event() # 当收满一个block时由本进程设置 |
| 229 | + self.stop_evt = Event() # 当主进程停止音频活动时由主进程设置 |
| 230 | + |
| 231 | + self.latency = Value('d', 114514.1919810) |
| 232 | + |
| 233 | + self.buf_shape: tuple = (self.buf_size, self.channels) |
| 234 | + self.buf_dtype: np.dtype = np.float32 |
| 235 | + self.buf_nbytes: int = int( |
| 236 | + np.prod(self.buf_shape) * np.dtype(self.buf_dtype).itemsize) |
| 237 | + |
| 238 | + self.in_mem = SharedMemory(create=True, size=self.buf_nbytes) |
| 239 | + self.out_mem = SharedMemory(create=True, size=self.buf_nbytes) |
| 240 | + self.in_mem_name: str = self.in_mem.name |
| 241 | + self.out_mem_name: str = self.out_mem.name |
| 242 | + |
| 243 | + self.in_buf = None |
| 244 | + self.out_buf = None |
| 245 | + |
| 246 | + def get_in_mem_name(self) -> str: |
| 247 | + return self.in_mem_name |
| 248 | + |
| 249 | + def get_out_mem_name(self) -> str: |
| 250 | + return self.out_mem_name |
| 251 | + |
| 252 | + def get_np_shape(self) -> tuple: |
| 253 | + return self.buf_shape |
| 254 | + |
| 255 | + def get_np_dtype(self) -> np.dtype: |
| 256 | + return self.buf_dtype |
| 257 | + |
| 258 | + def get_ptrs_and_events(self): |
| 259 | + return self.in_ptr, \ |
| 260 | + self.out_ptr,\ |
| 261 | + self.play_ptr,\ |
| 262 | + self.in_evt, \ |
| 263 | + self.stop_evt\ |
| 264 | + |
| 265 | + def get_latency(self) -> float: |
| 266 | + return self.latency.value |
| 267 | + |
| 268 | + def run(self): |
| 269 | + import sounddevice as sd |
| 270 | + |
| 271 | + signal.signal(signal.SIGINT, signal.SIG_IGN) |
| 272 | + |
| 273 | + in_mem = SharedMemory(name=self.in_mem_name) |
| 274 | + self.in_buf = np.ndarray( |
| 275 | + self.buf_shape, dtype=self.buf_dtype, buffer=in_mem.buf, order='C') |
| 276 | + self.in_buf.fill(0.0) |
| 277 | + |
| 278 | + out_mem = SharedMemory(name=self.out_mem_name) |
| 279 | + self.out_buf = np.ndarray( |
| 280 | + self.buf_shape, dtype=self.buf_dtype, buffer=out_mem.buf, order='C') |
| 281 | + self.out_buf.fill(0.0) |
| 282 | + |
| 283 | + exclusive_settings = sd.WasapiSettings(exclusive=True) |
| 284 | + |
| 285 | + sd.default.device = (self.in_dev, self.out_dev) |
| 286 | + |
| 287 | + def output_callback(outdata, frames, time_info, status): |
| 288 | + play_ptr = self.play_ptr.value |
| 289 | + end_ptr = play_ptr + frames |
| 290 | + |
| 291 | + if end_ptr <= self.buf_size: |
| 292 | + outdata[:] = self.out_buf[play_ptr:end_ptr] |
| 293 | + else: |
| 294 | + first = self.buf_size - play_ptr |
| 295 | + second = end_ptr - self.buf_size |
| 296 | + outdata[:first] = self.out_buf[play_ptr:] |
| 297 | + outdata[first:] = self.out_buf[:second] |
| 298 | + |
| 299 | + self.play_ptr.value = end_ptr % self.buf_size |
| 300 | + |
| 301 | + def input_callback(indata, frames, time_info, status): |
| 302 | + # 收录输入数据 |
| 303 | + end_ptr = self.__rec_ptr + frames |
| 304 | + if end_ptr <= self.buf_size: # 整块拷贝 |
| 305 | + self.in_buf[self.__rec_ptr:end_ptr] = indata |
| 306 | + else: # 处理回绕 |
| 307 | + first = self.buf_size - self.__rec_ptr |
| 308 | + second = end_ptr - self.buf_size |
| 309 | + self.in_buf[self.__rec_ptr:] = indata[:first] |
| 310 | + self.in_buf[:second] = indata[first:] |
| 311 | + write_pos = self.__rec_ptr |
| 312 | + self.__rec_ptr = end_ptr % self.buf_size |
| 313 | + |
| 314 | + # 设置信号 |
| 315 | + if write_pos < self.block_size and self.__rec_ptr >= self.block_size: |
| 316 | + self.in_ptr.value = 0 |
| 317 | + self.in_evt.set() # 通知主线程来取甲缓冲 |
| 318 | + elif write_pos < self.buf_size and self.__rec_ptr < write_pos: |
| 319 | + self.in_ptr.value = self.block_size |
| 320 | + self.in_evt.set() # 通知主线程来取乙缓冲 |
| 321 | + |
| 322 | + def combined_callback(indata, outdata, frames, time_info, status): |
| 323 | + output_callback(outdata, frames, time_info, status) # 优先出声 |
| 324 | + input_callback(indata, frames, time_info, status) |
| 325 | + |
| 326 | + if self.is_device_combined: |
| 327 | + with sd.Stream( |
| 328 | + samplerate=self.sample_rate, |
| 329 | + channels=self.channels, |
| 330 | + dtype=self.buf_dtype, |
| 331 | + latency='low', |
| 332 | + extra_settings=exclusive_settings if |
| 333 | + self.is_input_wasapi_exclusive and |
| 334 | + self.is_output_wasapi_exclusive else None, |
| 335 | + callback=combined_callback |
| 336 | + ) as s: |
| 337 | + self.latency.value = s.latency[-1] |
| 338 | + self.stop_evt.wait() |
| 339 | + self.out_buf.fill(0.0) |
| 340 | + else: |
| 341 | + with sd.InputStream( |
| 342 | + samplerate=self.sample_rate, |
| 343 | + channels=self.channels, |
| 344 | + dtype=self.buf_dtype, |
| 345 | + latency='low', |
| 346 | + extra_settings=exclusive_settings if self.is_input_wasapi_exclusive else None, |
| 347 | + callback=input_callback |
| 348 | + ) as si, sd.OutputStream( |
| 349 | + samplerate=self.sample_rate, |
| 350 | + channels=self.channels, |
| 351 | + dtype=self.buf_dtype, |
| 352 | + latency='low', |
| 353 | + extra_settings=exclusive_settings if self.is_output_wasapi_exclusive else None, |
| 354 | + callback=output_callback |
| 355 | + ) as so: |
| 356 | + self.latency.value = si.latency[-1] + so.latency[-1] |
| 357 | + self.stop_evt.wait() |
| 358 | + self.out_buf.fill(0.0) |
| 359 | + |
| 360 | + # 清理共享内存 |
| 361 | + in_mem.close() |
| 362 | + out_mem.close() |
| 363 | + in_mem.unlink() |
| 364 | + out_mem.unlink() |
0 commit comments