deepgram.audio.microphone.microphone
1# Copyright 2023-2024 Deepgram SDK contributors. All Rights Reserved. 2# Use of this source code is governed by a MIT license that can be found in the LICENSE file. 3# SPDX-License-Identifier: MIT 4 5import inspect 6import asyncio 7import threading 8from typing import Optional, Callable, Union, TYPE_CHECKING 9import logging 10 11from ...utils import verboselogs 12 13from .constants import LOGGING, CHANNELS, RATE, CHUNK 14 15if TYPE_CHECKING: 16 import pyaudio 17 18 19class Microphone: # pylint: disable=too-many-instance-attributes 20 """ 21 This implements a microphone for local audio input. This uses PyAudio under the hood. 22 """ 23 24 _logger: verboselogs.VerboseLogger 25 26 _audio: Optional["pyaudio.PyAudio"] = None 27 _stream: Optional["pyaudio.Stream"] = None 28 29 _chunk: int 30 _rate: int 31 _format: int 32 _channels: int 33 _input_device_index: Optional[int] 34 _is_muted: bool 35 36 _asyncio_loop: asyncio.AbstractEventLoop 37 _asyncio_thread: Optional[threading.Thread] = None 38 _exit: threading.Event 39 40 _push_callback_org: Optional[Callable] = None 41 _push_callback: Optional[Callable] = None 42 43 def __init__( 44 self, 45 push_callback: Optional[Callable] = None, 46 verbose: int = LOGGING, 47 rate: int = RATE, 48 chunk: int = CHUNK, 49 channels: int = CHANNELS, 50 input_device_index: Optional[int] = None, 51 ): # pylint: disable=too-many-positional-arguments 52 # dynamic import of pyaudio as not to force the requirements on the SDK (and users) 53 import pyaudio # pylint: disable=import-outside-toplevel 54 55 self._logger = verboselogs.VerboseLogger(__name__) 56 self._logger.addHandler(logging.StreamHandler()) 57 self._logger.setLevel(verbose) 58 59 self._exit = threading.Event() 60 61 self._audio = pyaudio.PyAudio() 62 self._chunk = chunk 63 self._rate = rate 64 self._format = pyaudio.paInt16 65 self._channels = channels 66 self._is_muted = False 67 68 self._input_device_index = input_device_index 69 self._push_callback_org = push_callback 70 71 def _start_asyncio_loop(self) -> None: 72 self._asyncio_loop = asyncio.new_event_loop() 73 self._asyncio_loop.run_forever() 74 75 def is_active(self) -> bool: 76 """ 77 is_active - returns the state of the stream 78 79 Args: 80 None 81 82 Returns: 83 True if the stream is active, False otherwise 84 """ 85 self._logger.debug("Microphone.is_active ENTER") 86 87 if self._stream is None: 88 self._logger.error("stream is None") 89 self._logger.debug("Microphone.is_active LEAVE") 90 return False 91 92 val = self._stream.is_active() 93 self._logger.info("is_active: %s", val) 94 self._logger.info("is_exiting: %s", self._exit.is_set()) 95 self._logger.debug("Microphone.is_active LEAVE") 96 return val 97 98 def set_callback(self, push_callback: Callable) -> None: 99 """ 100 set_callback - sets the callback function to be called when data is received. 101 102 Args: 103 push_callback (Callable): The callback function to be called when data is received. 104 This should be the websocket send function. 105 106 Returns: 107 None 108 """ 109 self._push_callback_org = push_callback 110 111 def start(self) -> bool: 112 """ 113 starts - starts the microphone stream 114 115 Returns: 116 bool: True if the stream was started, False otherwise 117 """ 118 self._logger.debug("Microphone.start ENTER") 119 120 self._logger.info("format: %s", self._format) 121 self._logger.info("channels: %d", self._channels) 122 self._logger.info("rate: %d", self._rate) 123 self._logger.info("chunk: %d", self._chunk) 124 # self._logger.info("input_device_id: %d", self._input_device_index) 125 126 if self._push_callback_org is None: 127 self._logger.error("start failed. No callback set.") 128 self._logger.debug("Microphone.start LEAVE") 129 return False 130 131 if inspect.iscoroutinefunction(self._push_callback_org): 132 self._logger.verbose("async/await callback - wrapping") 133 # Run our own asyncio loop. 134 self._asyncio_thread = threading.Thread(target=self._start_asyncio_loop) 135 self._asyncio_thread.start() 136 137 self._push_callback = lambda data: ( 138 asyncio.run_coroutine_threadsafe( 139 self._push_callback_org(data), self._asyncio_loop 140 ).result() 141 if self._push_callback_org 142 else None 143 ) 144 else: 145 self._logger.verbose("regular threaded callback") 146 self._asyncio_thread = None 147 self._push_callback = self._push_callback_org 148 149 if self._audio is not None: 150 self._stream = self._audio.open( 151 format=self._format, 152 channels=self._channels, 153 rate=self._rate, 154 input=True, 155 output=False, 156 frames_per_buffer=self._chunk, 157 input_device_index=self._input_device_index, 158 stream_callback=self._callback, 159 ) 160 161 if self._stream is None: 162 self._logger.error("start failed. No stream created.") 163 self._logger.debug("Microphone.start LEAVE") 164 return False 165 166 self._exit.clear() 167 if self._stream is not None: 168 self._stream.start_stream() 169 170 self._logger.notice("start succeeded") 171 self._logger.debug("Microphone.start LEAVE") 172 return True 173 174 def mute(self) -> bool: 175 """ 176 mute - mutes the microphone stream 177 178 Returns: 179 bool: True if the stream was muted, False otherwise 180 """ 181 self._logger.verbose("Microphone.mute ENTER") 182 183 if self._stream is None: 184 self._logger.error("mute failed. Library not initialized.") 185 self._logger.verbose("Microphone.mute LEAVE") 186 return False 187 188 self._is_muted = True 189 190 self._logger.notice("mute succeeded") 191 self._logger.verbose("Microphone.mute LEAVE") 192 return True 193 194 def unmute(self) -> bool: 195 """ 196 unmute - unmutes the microphone stream 197 198 Returns: 199 bool: True if the stream was unmuted, False otherwise 200 """ 201 self._logger.verbose("Microphone.unmute ENTER") 202 203 if self._stream is None: 204 self._logger.error("unmute failed. Library not initialized.") 205 self._logger.verbose("Microphone.unmute LEAVE") 206 return False 207 208 self._is_muted = False 209 210 self._logger.notice("unmute succeeded") 211 self._logger.verbose("Microphone.unmute LEAVE") 212 return True 213 214 def is_muted(self) -> bool: 215 """ 216 is_muted - returns the state of the stream 217 218 Args: 219 None 220 221 Returns: 222 True if the stream is muted, False otherwise 223 """ 224 self._logger.spam("Microphone.is_muted ENTER") 225 226 if self._stream is None: 227 self._logger.spam("is_muted: stream is None") 228 self._logger.spam("Microphone.is_muted LEAVE") 229 return False 230 231 val = self._is_muted 232 233 self._logger.spam("is_muted: %s", val) 234 self._logger.spam("Microphone.is_muted LEAVE") 235 return val 236 237 def finish(self) -> bool: 238 """ 239 finish - stops the microphone stream 240 241 Returns: 242 bool: True if the stream was stopped, False otherwise 243 """ 244 self._logger.debug("Microphone.finish ENTER") 245 246 self._logger.notice("signal exit") 247 self._exit.set() 248 249 # Stop the stream. 250 if self._stream is not None: 251 self._logger.notice("stopping stream...") 252 self._stream.stop_stream() 253 self._stream.close() 254 self._logger.notice("stream stopped") 255 256 # clean up the thread 257 if ( 258 # inspect.iscoroutinefunction(self._push_callback_org) 259 # and 260 self._asyncio_thread 261 is not None 262 ): 263 self._logger.notice("stopping _asyncio_loop...") 264 self._asyncio_loop.call_soon_threadsafe(self._asyncio_loop.stop) 265 self._asyncio_thread.join() 266 self._logger.notice("_asyncio_thread joined") 267 self._stream = None 268 self._asyncio_thread = None 269 270 self._logger.notice("finish succeeded") 271 self._logger.debug("Microphone.finish LEAVE") 272 273 return True 274 275 def _callback( 276 self, input_data, frame_count, time_info, status_flags 277 ): # pylint: disable=unused-argument 278 """ 279 The callback used to process data in callback mode. 280 """ 281 # dynamic import of pyaudio as not to force the requirements on the SDK (and users) 282 import pyaudio # pylint: disable=import-outside-toplevel 283 284 if self._exit.is_set(): 285 self._logger.notice("_callback exit is Set. stopping...") 286 return None, pyaudio.paAbort 287 288 if input_data is None: 289 self._logger.warning("input_data is None") 290 return None, pyaudio.paContinue 291 292 try: 293 if self._is_muted: 294 size = len(input_data) 295 input_data = b"\x00" * size 296 297 self._push_callback(input_data) 298 except Exception as e: 299 self._logger.error("Error while sending: %s", str(e)) 300 raise 301 302 return input_data, pyaudio.paContinue
20class Microphone: # pylint: disable=too-many-instance-attributes 21 """ 22 This implements a microphone for local audio input. This uses PyAudio under the hood. 23 """ 24 25 _logger: verboselogs.VerboseLogger 26 27 _audio: Optional["pyaudio.PyAudio"] = None 28 _stream: Optional["pyaudio.Stream"] = None 29 30 _chunk: int 31 _rate: int 32 _format: int 33 _channels: int 34 _input_device_index: Optional[int] 35 _is_muted: bool 36 37 _asyncio_loop: asyncio.AbstractEventLoop 38 _asyncio_thread: Optional[threading.Thread] = None 39 _exit: threading.Event 40 41 _push_callback_org: Optional[Callable] = None 42 _push_callback: Optional[Callable] = None 43 44 def __init__( 45 self, 46 push_callback: Optional[Callable] = None, 47 verbose: int = LOGGING, 48 rate: int = RATE, 49 chunk: int = CHUNK, 50 channels: int = CHANNELS, 51 input_device_index: Optional[int] = None, 52 ): # pylint: disable=too-many-positional-arguments 53 # dynamic import of pyaudio as not to force the requirements on the SDK (and users) 54 import pyaudio # pylint: disable=import-outside-toplevel 55 56 self._logger = verboselogs.VerboseLogger(__name__) 57 self._logger.addHandler(logging.StreamHandler()) 58 self._logger.setLevel(verbose) 59 60 self._exit = threading.Event() 61 62 self._audio = pyaudio.PyAudio() 63 self._chunk = chunk 64 self._rate = rate 65 self._format = pyaudio.paInt16 66 self._channels = channels 67 self._is_muted = False 68 69 self._input_device_index = input_device_index 70 self._push_callback_org = push_callback 71 72 def _start_asyncio_loop(self) -> None: 73 self._asyncio_loop = asyncio.new_event_loop() 74 self._asyncio_loop.run_forever() 75 76 def is_active(self) -> bool: 77 """ 78 is_active - returns the state of the stream 79 80 Args: 81 None 82 83 Returns: 84 True if the stream is active, False otherwise 85 """ 86 self._logger.debug("Microphone.is_active ENTER") 87 88 if self._stream is None: 89 self._logger.error("stream is None") 90 self._logger.debug("Microphone.is_active LEAVE") 91 return False 92 93 val = self._stream.is_active() 94 self._logger.info("is_active: %s", val) 95 self._logger.info("is_exiting: %s", self._exit.is_set()) 96 self._logger.debug("Microphone.is_active LEAVE") 97 return val 98 99 def set_callback(self, push_callback: Callable) -> None: 100 """ 101 set_callback - sets the callback function to be called when data is received. 102 103 Args: 104 push_callback (Callable): The callback function to be called when data is received. 105 This should be the websocket send function. 106 107 Returns: 108 None 109 """ 110 self._push_callback_org = push_callback 111 112 def start(self) -> bool: 113 """ 114 starts - starts the microphone stream 115 116 Returns: 117 bool: True if the stream was started, False otherwise 118 """ 119 self._logger.debug("Microphone.start ENTER") 120 121 self._logger.info("format: %s", self._format) 122 self._logger.info("channels: %d", self._channels) 123 self._logger.info("rate: %d", self._rate) 124 self._logger.info("chunk: %d", self._chunk) 125 # self._logger.info("input_device_id: %d", self._input_device_index) 126 127 if self._push_callback_org is None: 128 self._logger.error("start failed. No callback set.") 129 self._logger.debug("Microphone.start LEAVE") 130 return False 131 132 if inspect.iscoroutinefunction(self._push_callback_org): 133 self._logger.verbose("async/await callback - wrapping") 134 # Run our own asyncio loop. 135 self._asyncio_thread = threading.Thread(target=self._start_asyncio_loop) 136 self._asyncio_thread.start() 137 138 self._push_callback = lambda data: ( 139 asyncio.run_coroutine_threadsafe( 140 self._push_callback_org(data), self._asyncio_loop 141 ).result() 142 if self._push_callback_org 143 else None 144 ) 145 else: 146 self._logger.verbose("regular threaded callback") 147 self._asyncio_thread = None 148 self._push_callback = self._push_callback_org 149 150 if self._audio is not None: 151 self._stream = self._audio.open( 152 format=self._format, 153 channels=self._channels, 154 rate=self._rate, 155 input=True, 156 output=False, 157 frames_per_buffer=self._chunk, 158 input_device_index=self._input_device_index, 159 stream_callback=self._callback, 160 ) 161 162 if self._stream is None: 163 self._logger.error("start failed. No stream created.") 164 self._logger.debug("Microphone.start LEAVE") 165 return False 166 167 self._exit.clear() 168 if self._stream is not None: 169 self._stream.start_stream() 170 171 self._logger.notice("start succeeded") 172 self._logger.debug("Microphone.start LEAVE") 173 return True 174 175 def mute(self) -> bool: 176 """ 177 mute - mutes the microphone stream 178 179 Returns: 180 bool: True if the stream was muted, False otherwise 181 """ 182 self._logger.verbose("Microphone.mute ENTER") 183 184 if self._stream is None: 185 self._logger.error("mute failed. Library not initialized.") 186 self._logger.verbose("Microphone.mute LEAVE") 187 return False 188 189 self._is_muted = True 190 191 self._logger.notice("mute succeeded") 192 self._logger.verbose("Microphone.mute LEAVE") 193 return True 194 195 def unmute(self) -> bool: 196 """ 197 unmute - unmutes the microphone stream 198 199 Returns: 200 bool: True if the stream was unmuted, False otherwise 201 """ 202 self._logger.verbose("Microphone.unmute ENTER") 203 204 if self._stream is None: 205 self._logger.error("unmute failed. Library not initialized.") 206 self._logger.verbose("Microphone.unmute LEAVE") 207 return False 208 209 self._is_muted = False 210 211 self._logger.notice("unmute succeeded") 212 self._logger.verbose("Microphone.unmute LEAVE") 213 return True 214 215 def is_muted(self) -> bool: 216 """ 217 is_muted - returns the state of the stream 218 219 Args: 220 None 221 222 Returns: 223 True if the stream is muted, False otherwise 224 """ 225 self._logger.spam("Microphone.is_muted ENTER") 226 227 if self._stream is None: 228 self._logger.spam("is_muted: stream is None") 229 self._logger.spam("Microphone.is_muted LEAVE") 230 return False 231 232 val = self._is_muted 233 234 self._logger.spam("is_muted: %s", val) 235 self._logger.spam("Microphone.is_muted LEAVE") 236 return val 237 238 def finish(self) -> bool: 239 """ 240 finish - stops the microphone stream 241 242 Returns: 243 bool: True if the stream was stopped, False otherwise 244 """ 245 self._logger.debug("Microphone.finish ENTER") 246 247 self._logger.notice("signal exit") 248 self._exit.set() 249 250 # Stop the stream. 251 if self._stream is not None: 252 self._logger.notice("stopping stream...") 253 self._stream.stop_stream() 254 self._stream.close() 255 self._logger.notice("stream stopped") 256 257 # clean up the thread 258 if ( 259 # inspect.iscoroutinefunction(self._push_callback_org) 260 # and 261 self._asyncio_thread 262 is not None 263 ): 264 self._logger.notice("stopping _asyncio_loop...") 265 self._asyncio_loop.call_soon_threadsafe(self._asyncio_loop.stop) 266 self._asyncio_thread.join() 267 self._logger.notice("_asyncio_thread joined") 268 self._stream = None 269 self._asyncio_thread = None 270 271 self._logger.notice("finish succeeded") 272 self._logger.debug("Microphone.finish LEAVE") 273 274 return True 275 276 def _callback( 277 self, input_data, frame_count, time_info, status_flags 278 ): # pylint: disable=unused-argument 279 """ 280 The callback used to process data in callback mode. 281 """ 282 # dynamic import of pyaudio as not to force the requirements on the SDK (and users) 283 import pyaudio # pylint: disable=import-outside-toplevel 284 285 if self._exit.is_set(): 286 self._logger.notice("_callback exit is Set. stopping...") 287 return None, pyaudio.paAbort 288 289 if input_data is None: 290 self._logger.warning("input_data is None") 291 return None, pyaudio.paContinue 292 293 try: 294 if self._is_muted: 295 size = len(input_data) 296 input_data = b"\x00" * size 297 298 self._push_callback(input_data) 299 except Exception as e: 300 self._logger.error("Error while sending: %s", str(e)) 301 raise 302 303 return input_data, pyaudio.paContinue
This implements a microphone for local audio input. This uses PyAudio under the hood.
44 def __init__( 45 self, 46 push_callback: Optional[Callable] = None, 47 verbose: int = LOGGING, 48 rate: int = RATE, 49 chunk: int = CHUNK, 50 channels: int = CHANNELS, 51 input_device_index: Optional[int] = None, 52 ): # pylint: disable=too-many-positional-arguments 53 # dynamic import of pyaudio as not to force the requirements on the SDK (and users) 54 import pyaudio # pylint: disable=import-outside-toplevel 55 56 self._logger = verboselogs.VerboseLogger(__name__) 57 self._logger.addHandler(logging.StreamHandler()) 58 self._logger.setLevel(verbose) 59 60 self._exit = threading.Event() 61 62 self._audio = pyaudio.PyAudio() 63 self._chunk = chunk 64 self._rate = rate 65 self._format = pyaudio.paInt16 66 self._channels = channels 67 self._is_muted = False 68 69 self._input_device_index = input_device_index 70 self._push_callback_org = push_callback
76 def is_active(self) -> bool: 77 """ 78 is_active - returns the state of the stream 79 80 Args: 81 None 82 83 Returns: 84 True if the stream is active, False otherwise 85 """ 86 self._logger.debug("Microphone.is_active ENTER") 87 88 if self._stream is None: 89 self._logger.error("stream is None") 90 self._logger.debug("Microphone.is_active LEAVE") 91 return False 92 93 val = self._stream.is_active() 94 self._logger.info("is_active: %s", val) 95 self._logger.info("is_exiting: %s", self._exit.is_set()) 96 self._logger.debug("Microphone.is_active LEAVE") 97 return val
is_active - returns the state of the stream
Args: None
Returns: True if the stream is active, False otherwise
99 def set_callback(self, push_callback: Callable) -> None: 100 """ 101 set_callback - sets the callback function to be called when data is received. 102 103 Args: 104 push_callback (Callable): The callback function to be called when data is received. 105 This should be the websocket send function. 106 107 Returns: 108 None 109 """ 110 self._push_callback_org = push_callback
set_callback - sets the callback function to be called when data is received.
Args: push_callback (Callable): The callback function to be called when data is received. This should be the websocket send function.
Returns: None
112 def start(self) -> bool: 113 """ 114 starts - starts the microphone stream 115 116 Returns: 117 bool: True if the stream was started, False otherwise 118 """ 119 self._logger.debug("Microphone.start ENTER") 120 121 self._logger.info("format: %s", self._format) 122 self._logger.info("channels: %d", self._channels) 123 self._logger.info("rate: %d", self._rate) 124 self._logger.info("chunk: %d", self._chunk) 125 # self._logger.info("input_device_id: %d", self._input_device_index) 126 127 if self._push_callback_org is None: 128 self._logger.error("start failed. No callback set.") 129 self._logger.debug("Microphone.start LEAVE") 130 return False 131 132 if inspect.iscoroutinefunction(self._push_callback_org): 133 self._logger.verbose("async/await callback - wrapping") 134 # Run our own asyncio loop. 135 self._asyncio_thread = threading.Thread(target=self._start_asyncio_loop) 136 self._asyncio_thread.start() 137 138 self._push_callback = lambda data: ( 139 asyncio.run_coroutine_threadsafe( 140 self._push_callback_org(data), self._asyncio_loop 141 ).result() 142 if self._push_callback_org 143 else None 144 ) 145 else: 146 self._logger.verbose("regular threaded callback") 147 self._asyncio_thread = None 148 self._push_callback = self._push_callback_org 149 150 if self._audio is not None: 151 self._stream = self._audio.open( 152 format=self._format, 153 channels=self._channels, 154 rate=self._rate, 155 input=True, 156 output=False, 157 frames_per_buffer=self._chunk, 158 input_device_index=self._input_device_index, 159 stream_callback=self._callback, 160 ) 161 162 if self._stream is None: 163 self._logger.error("start failed. No stream created.") 164 self._logger.debug("Microphone.start LEAVE") 165 return False 166 167 self._exit.clear() 168 if self._stream is not None: 169 self._stream.start_stream() 170 171 self._logger.notice("start succeeded") 172 self._logger.debug("Microphone.start LEAVE") 173 return True
starts - starts the microphone stream
Returns: bool: True if the stream was started, False otherwise
175 def mute(self) -> bool: 176 """ 177 mute - mutes the microphone stream 178 179 Returns: 180 bool: True if the stream was muted, False otherwise 181 """ 182 self._logger.verbose("Microphone.mute ENTER") 183 184 if self._stream is None: 185 self._logger.error("mute failed. Library not initialized.") 186 self._logger.verbose("Microphone.mute LEAVE") 187 return False 188 189 self._is_muted = True 190 191 self._logger.notice("mute succeeded") 192 self._logger.verbose("Microphone.mute LEAVE") 193 return True
mute - mutes the microphone stream
Returns: bool: True if the stream was muted, False otherwise
195 def unmute(self) -> bool: 196 """ 197 unmute - unmutes the microphone stream 198 199 Returns: 200 bool: True if the stream was unmuted, False otherwise 201 """ 202 self._logger.verbose("Microphone.unmute ENTER") 203 204 if self._stream is None: 205 self._logger.error("unmute failed. Library not initialized.") 206 self._logger.verbose("Microphone.unmute LEAVE") 207 return False 208 209 self._is_muted = False 210 211 self._logger.notice("unmute succeeded") 212 self._logger.verbose("Microphone.unmute LEAVE") 213 return True
unmute - unmutes the microphone stream
Returns: bool: True if the stream was unmuted, False otherwise
215 def is_muted(self) -> bool: 216 """ 217 is_muted - returns the state of the stream 218 219 Args: 220 None 221 222 Returns: 223 True if the stream is muted, False otherwise 224 """ 225 self._logger.spam("Microphone.is_muted ENTER") 226 227 if self._stream is None: 228 self._logger.spam("is_muted: stream is None") 229 self._logger.spam("Microphone.is_muted LEAVE") 230 return False 231 232 val = self._is_muted 233 234 self._logger.spam("is_muted: %s", val) 235 self._logger.spam("Microphone.is_muted LEAVE") 236 return val
is_muted - returns the state of the stream
Args: None
Returns: True if the stream is muted, False otherwise
238 def finish(self) -> bool: 239 """ 240 finish - stops the microphone stream 241 242 Returns: 243 bool: True if the stream was stopped, False otherwise 244 """ 245 self._logger.debug("Microphone.finish ENTER") 246 247 self._logger.notice("signal exit") 248 self._exit.set() 249 250 # Stop the stream. 251 if self._stream is not None: 252 self._logger.notice("stopping stream...") 253 self._stream.stop_stream() 254 self._stream.close() 255 self._logger.notice("stream stopped") 256 257 # clean up the thread 258 if ( 259 # inspect.iscoroutinefunction(self._push_callback_org) 260 # and 261 self._asyncio_thread 262 is not None 263 ): 264 self._logger.notice("stopping _asyncio_loop...") 265 self._asyncio_loop.call_soon_threadsafe(self._asyncio_loop.stop) 266 self._asyncio_thread.join() 267 self._logger.notice("_asyncio_thread joined") 268 self._stream = None 269 self._asyncio_thread = None 270 271 self._logger.notice("finish succeeded") 272 self._logger.debug("Microphone.finish LEAVE") 273 274 return True
finish - stops the microphone stream
Returns: bool: True if the stream was stopped, False otherwise