From cd2ea546b8bf3bb1c76271cc8f4e9c15ecd940e0 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 14 Jul 2025 22:06:36 -0400 Subject: [PATCH] revert noisegate --- app/internal/NoiseGatev2.py | 160 ++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 90 deletions(-) diff --git a/app/internal/NoiseGatev2.py b/app/internal/NoiseGatev2.py index ae29542..ec52ec7 100644 --- a/app/internal/NoiseGatev2.py +++ b/app/internal/NoiseGatev2.py @@ -2,30 +2,77 @@ import audioop import math import pyaudio import asyncio -from discord import VoiceClient from internal.logger import create_logger -from discord.opus import OpusNotLoaded +from discord import AudioSource LOGGER = create_logger(__name__) # Constants for audio processing -SAMPLES_PER_FRAME = 960 # For 20ms audio at 48kHz +SAMPLES_PER_FRAME = 960 CHANNELS = 2 SAMPLE_RATE = 48000 -FRAME_SIZE = SAMPLES_PER_FRAME * CHANNELS * 2 # 16-bit PCM (2 bytes) +FRAME_SIZE = SAMPLES_PER_FRAME * CHANNELS * 2 # 16-bit PCM +SILENT_FRAME = b'\x00' * FRAME_SIZE -class AudioTransmitter: - def __init__(self, voice_client: VoiceClient, noise_gate_threshold: int, loop: asyncio.AbstractEventLoop, input_device_index: int): - if not voice_client.is_connected(): - raise ValueError("VoiceClient is not connected.") - if not hasattr(voice_client, 'encoder') or not voice_client.encoder: - raise OpusNotLoaded("VoiceClient has not initialized its Opus encoder.") - self.voice_client = voice_client - self.threshold = noise_gate_threshold - self.loop = loop - self.input_device_index = input_device_index +class NoiseGateSource(AudioSource): + def __init__(self, audio_stream, threshold: int): + self.audio_stream = audio_stream + self.threshold = threshold + self.ng_fadeout_count = 0 + self.NG_FADEOUT_FRAMES = 12 # 240ms fadeout time + def read(self) -> bytes: + """ + Reads data from the audio stream, applies the noise gate, + and returns a 20ms audio frame. + """ + try: + # Read a frame's worth of data from the input stream. + pcm_data = self.audio_stream.read(SAMPLES_PER_FRAME, exception_on_overflow=False) + + # Ensure we have a full frame of data. + if len(pcm_data) != FRAME_SIZE: + return + + # Calculate volume to check against the threshold. + rms = audioop.rms(pcm_data, 2) + if rms == 0: + # If there's no volume, check if we're in the fadeout period. + if self.ng_fadeout_count > 0: + self.ng_fadeout_count -= 1 + return pcm_data # Return the (silent) data to complete the fade + return + + db = 20 * math.log10(rms) + + # If volume is above the threshold, send the audio and reset fadeout. + if db >= self.threshold: + self.ng_fadeout_count = self.NG_FADEOUT_FRAMES + return pcm_data + + # If below threshold but still in the fadeout period, send the audio. + if self.ng_fadeout_count > 0: + self.ng_fadeout_count -= 1 + return pcm_data + + # Otherwise, the gate is closed. + return + + except Exception as e: + LOGGER.error(f"Error in NoiseGateSource.read: {e}", exc_info=True) + return SILENT_FRAME + + def cleanup(self) -> None: + """Called when the player stops.""" + if self.audio_stream: + self.audio_stream.stop_stream() + self.audio_stream.close() + LOGGER.info("Audio stream cleaned up.") + +class AudioStreamManager: + """Manages the PyAudio instance and input stream.""" + def __init__(self, input_device_index: int): self.pa = pyaudio.PyAudio() self.stream = self.pa.open( format=pyaudio.paInt16, @@ -33,84 +80,17 @@ class AudioTransmitter: rate=SAMPLE_RATE, input=True, frames_per_buffer=SAMPLES_PER_FRAME, - input_device_index=self.input_device_index + input_device_index=input_device_index ) - - self._is_running = False - self._is_speaking = False - self.ng_fadeout_count = 0 - self.NG_FADEOUT_FRAMES = 12 # 240ms fadeout time - - async def _set_speaking(self, speaking: bool): - """Safely sets the speaking state if it has changed.""" - if self._is_speaking != speaking: - self._is_speaking = speaking - await self.voice_client.ws.speak(speaking) - - async def start(self): - """Starts the main audio transmission loop.""" - self._is_running = True self.stream.start_stream() - LOGGER.info("Audio transmitter started.") - try: - while self._is_running: - # Read audio data in a separate thread to not block the event loop - pcm_data = await self.loop.run_in_executor( - None, self.stream.read, SAMPLES_PER_FRAME - ) + LOGGER.info(f"Audio stream started on device {input_device_index}") - gate_is_open = self._check_noise_gate(pcm_data) + def get_stream(self): + return self.stream - if gate_is_open: - # If gate is open, ensure speaking is on and send audio - await self._set_speaking(True) - - # Encode PCM data to Opus - encoded_packets = self.voice_client.encoder.encode(pcm_data, SAMPLES_PER_FRAME) - - # Send each encoded packet - for packet in encoded_packets: - self.voice_client.send_audio_packet(packet) - else: - # If gate is closed, ensure speaking is off - await self._set_speaking(False) - - # Wait for the next 20ms interval - await asyncio.sleep(0.02) - except asyncio.CancelledError: - LOGGER.info("Audio transmitter task cancelled.") - except Exception as e: - LOGGER.error(f"Error in audio transmitter loop: {e}", exc_info=True) - finally: - await self._cleanup() - - def _check_noise_gate(self, pcm_data: bytes) -> bool: - """Applies the noise gate logic to raw PCM data.""" - rms = audioop.rms(pcm_data, 2) - if rms == 0: return False - - db = 20 * math.log10(rms) - - if db >= self.threshold: - self.ng_fadeout_count = self.NG_FADEOUT_FRAMES - return True - elif self.ng_fadeout_count > 0: - self.ng_fadeout_count -= 1 - return True - - return False - - async def stop(self): - """Stops the transmission loop.""" - self._is_running = False - - async def _cleanup(self): - """Cleans up all resources.""" - LOGGER.info("Cleaning up transmitter resources.") - if self._is_speaking: - await self._set_speaking(False) - - if self.stream.is_active(): + def terminate(self): + if self.stream and self.stream.is_active(): self.stream.stop_stream() - self.stream.close() - self.pa.terminate() \ No newline at end of file + self.stream.close() + self.pa.terminate() + LOGGER.info("PyAudio instance terminated.") \ No newline at end of file