Revert voice activity changes

This commit is contained in:
Logan Cusano
2025-07-14 22:25:36 -04:00
parent 84cef3119f
commit aee6e40792
2 changed files with 32 additions and 141 deletions

View File

@@ -3,6 +3,9 @@ import math
import pyaudio import pyaudio
import asyncio import asyncio
from internal.logger import create_logger from internal.logger import create_logger
# You need to import the base AudioSource class from your specific library.
# This is a common path, but yours might be different.
from discord import AudioSource from discord import AudioSource
LOGGER = create_logger(__name__) LOGGER = create_logger(__name__)
@@ -33,7 +36,7 @@ class NoiseGateSource(AudioSource):
# Ensure we have a full frame of data. # Ensure we have a full frame of data.
if len(pcm_data) != FRAME_SIZE: if len(pcm_data) != FRAME_SIZE:
return return SILENT_FRAME
# Calculate volume to check against the threshold. # Calculate volume to check against the threshold.
rms = audioop.rms(pcm_data, 2) rms = audioop.rms(pcm_data, 2)
@@ -42,7 +45,7 @@ class NoiseGateSource(AudioSource):
if self.ng_fadeout_count > 0: if self.ng_fadeout_count > 0:
self.ng_fadeout_count -= 1 self.ng_fadeout_count -= 1
return pcm_data # Return the (silent) data to complete the fade return pcm_data # Return the (silent) data to complete the fade
return return SILENT_FRAME
db = 20 * math.log10(rms) db = 20 * math.log10(rms)
@@ -56,8 +59,8 @@ class NoiseGateSource(AudioSource):
self.ng_fadeout_count -= 1 self.ng_fadeout_count -= 1
return pcm_data return pcm_data
# Otherwise, the gate is closed. # Otherwise, the gate is closed. Send silence.
return return SILENT_FRAME
except Exception as e: except Exception as e:
LOGGER.error(f"Error in NoiseGateSource.read: {e}", exc_info=True) LOGGER.error(f"Error in NoiseGateSource.read: {e}", exc_info=True)
@@ -65,10 +68,9 @@ class NoiseGateSource(AudioSource):
def cleanup(self) -> None: def cleanup(self) -> None:
"""Called when the player stops.""" """Called when the player stops."""
if self.audio_stream: # The AudioStreamManager now handles cleanup.
self.audio_stream.stop_stream() LOGGER.info("Audio source cleanup called.")
self.audio_stream.close() pass
LOGGER.info("Audio stream cleaned up.")
class AudioStreamManager: class AudioStreamManager:
"""Manages the PyAudio instance and input stream.""" """Manages the PyAudio instance and input stream."""
@@ -92,102 +94,5 @@ class AudioStreamManager:
if self.stream and self.stream.is_active(): if self.stream and self.stream.is_active():
self.stream.stop_stream() self.stream.stop_stream()
self.stream.close() self.stream.close()
LOGGER.debug("[ReopenStream.close_if_open]:\t Stream was open; It was closed.") self.pa.terminate()
LOGGER.info("PyAudio instance terminated.")
def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
LOGGER.info('Getting a list of the devices connected')
info = self.paInstance.get_host_api_info_by_index(0)
numdevices = info.get('deviceCount')
devices = {'Input': {}, 'Output': {}}
for i in range(0, numdevices):
device_info = self.paInstance.get_device_info_by_host_api_device_index(0, i)
if (device_info.get('maxInputChannels')) > 0:
input_device = device_info.get('name')
devices['Input'][i] = input_device
if _display_input_devices:
LOGGER.debug(f"Input Device id {i} - {input_device}")
if (device_info.get('maxOutputChannels')) > 0:
output_device = device_info.get('name')
devices['Output'][i] = output_device
if _display_output_devices:
LOGGER.debug(f"Output Device id {i} - {output_device}")
return devices
# noinspection PyUnresolvedReferences
class NoiseGate(AudioStream):
def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs):
super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs)
self.voice_connection = _voice_connection
self.THRESHOLD = _noise_gate_threshold
self.NGStream = NoiseGateStream(self)
def run(self) -> None:
LOGGER.debug("Starting stream")
self.stream.start_stream()
self.core()
def core(self):
if self.voice_connection.is_connected() and not self.voice_connection.is_playing():
LOGGER.debug("Playing stream to discord")
self.voice_connection.play(self.NGStream)
async def close(self):
LOGGER.debug("Closing NoiseGate resources...")
if self.voice_connection and self.voice_connection.is_connected():
self.voice_connection.stop()
self.close_if_open()
if self.paInstance:
self.paInstance.terminate()
LOGGER.debug("NoiseGate resources closed.")
# noinspection PyUnresolvedReferences
class NoiseGateStream(discord.AudioSource):
def __init__(self, noise_gate_instance: NoiseGate):
super(NoiseGateStream, self).__init__()
self.noise_gate = noise_gate_instance
self.NG_fadeout = 12
self.NG_fadeout_count = 0
self.process_set_count = 0
def read(self):
try:
if not self.noise_gate.voice_connection.is_connected():
return SILENT_FRAME
curr_buffer = self.noise_gate.stream.read(960, exception_on_overflow=False)
if len(curr_buffer) != DISCORD_FRAME_SIZE:
return SILENT_FRAME
buffer_rms = audioop.rms(curr_buffer, 2)
if buffer_rms > 0:
buffer_decibel = 20 * math.log10(buffer_rms)
if self.process_set_count % 10 == 0:
log_msg = f"[{'Open' if buffer_decibel >= self.noise_gate.THRESHOLD else 'Closed'}]"
LOGGER.debug(f"[NoiseGate {log_msg}] {buffer_decibel:.2f} dB")
if buffer_decibel >= self.noise_gate.THRESHOLD:
self.NG_fadeout_count = self.NG_fadeout
self.process_set_count += 1
return bytes(curr_buffer)
elif self.NG_fadeout_count > 0:
self.NG_fadeout_count -= 1
self.process_set_count += 1
return bytes(curr_buffer)
return SILENT_FRAME
except IOError as e:
LOGGER.error(f"PyAudio IOError in read(): {e}")
return SILENT_FRAME
except Exception as e:
LOGGER.error(f"Unhandled exception in NoiseGateStream.read: {e}", exc_info=True)
return SILENT_FRAME

View File

@@ -4,12 +4,11 @@ import os
from discord import VoiceClient, VoiceChannel, opus, Activity, ActivityType, Intents from discord import VoiceClient, VoiceChannel, opus, Activity, ActivityType, Intents
from discord.ext import commands from discord.ext import commands
from typing import Optional, Dict from typing import Optional, Dict
from internal.NoiseGatev2 import AudioStreamManager, NoiseGateSource
from internal.logger import create_logger from internal.logger import create_logger
from internal.NoiseGatev2 import AudioStreamManager, NoiseGateSource
LOGGER = create_logger(__name__) LOGGER = create_logger(__name__)
# Configure discord intents
intents = Intents.default() intents = Intents.default()
intents.voice_states = True intents.voice_states = True
intents.guilds = True intents.guilds = True
@@ -42,18 +41,13 @@ class DiscordBotManager:
@self.bot.event @self.bot.event
async def on_voice_state_update(member, before, after): async def on_voice_state_update(member, before, after):
if member != self.bot.user: if member != self.bot.user: return
return
if before.channel is None and after.channel is not None: if before.channel is None and after.channel is not None:
LOGGER.info(f"{member.name} joined voice channel {after.channel.name}") LOGGER.info(f"{member.name} joined voice channel {after.channel.name}")
self._voice_ready_event.set() self._voice_ready_event.set()
elif before.channel is not None and after.channel is not None and before.channel != after.channel: elif before.channel is not None and after.channel is not None and before.channel != after.channel:
LOGGER.info(f"{member.name} was moved to voice channel {after.channel.name}") LOGGER.info(f"{member.name} was moved to voice channel {after.channel.name}")
if not self._voice_ready_event.is_set(): if not self._voice_ready_event.is_set(): self._voice_ready_event.set()
self._voice_ready_event.set()
elif before.channel is not None and after.channel is None: elif before.channel is not None and after.channel is None:
LOGGER.warning(f"{member.name} left voice channel {before.channel.name}") LOGGER.warning(f"{member.name} left voice channel {before.channel.name}")
guild_id = before.channel.guild.id guild_id = before.channel.guild.id
@@ -75,8 +69,7 @@ class DiscordBotManager:
LOGGER.info("Bot is ready.") LOGGER.info("Bot is ready.")
except asyncio.TimeoutError: except asyncio.TimeoutError:
LOGGER.error("Timeout waiting for bot to become ready.") LOGGER.error("Timeout waiting for bot to become ready.")
if self.bot_task and not self.bot_task.done(): if self.bot_task and not self.bot_task.done(): self.bot_task.cancel()
self.bot_task.cancel()
raise RuntimeError("Bot failed to become ready within timeout.") raise RuntimeError("Bot failed to become ready within timeout.")
async def stop_bot(self): async def stop_bot(self):
@@ -107,14 +100,10 @@ class DiscordBotManager:
voice_client = await channel.connect(timeout=60.0, reconnect=True) voice_client = await channel.connect(timeout=60.0, reconnect=True)
await asyncio.wait_for(self._voice_ready_event.wait(), timeout=15.0) await asyncio.wait_for(self._voice_ready_event.wait(), timeout=15.0)
# Create a single audio manager for this connection
audio_manager = AudioStreamManager(input_device_index=device_id) audio_manager = AudioStreamManager(input_device_index=device_id)
# Create the noise-gated audio source
audio_source = NoiseGateSource(audio_manager.get_stream(), threshold=ng_threshold) audio_source = NoiseGateSource(audio_manager.get_stream(), threshold=ng_threshold)
# Play the source voice_client.play(audio_source, after=lambda e: LOGGER.error(f'Player error: {e}') if e else None)
voice_client.play(audio_source, after=lambda e: print(f'Player error: {e}') if e else None)
self.voice_connections[guild_id] = { self.voice_connections[guild_id] = {
"client": voice_client, "client": voice_client,
@@ -124,6 +113,8 @@ class DiscordBotManager:
except Exception as e: except Exception as e:
LOGGER.error(f"Failed to connect to voice channel: {e}", exc_info=True) LOGGER.error(f"Failed to connect to voice channel: {e}", exc_info=True)
if guild_id in self.voice_connections: # Cleanup if join fails midway
await self.leave_voice_channel(guild_id)
raise raise
async def leave_voice_channel(self, guild_id: int): async def leave_voice_channel(self, guild_id: int):
@@ -137,19 +128,23 @@ class DiscordBotManager:
voice_client.stop() voice_client.stop()
await voice_client.disconnect() await voice_client.disconnect()
# Terminate the audio manager to release PyAudio resources
audio_manager = connection_info.get("audio_manager") audio_manager = connection_info.get("audio_manager")
if audio_manager: if audio_manager:
audio_manager.terminate() audio_manager.terminate()
del self.voice_connections[guild_id] # Use pop to safely remove the key
self.voice_connections.pop(guild_id, None)
LOGGER.info(f"Left guild {guild_id} voice channel.") LOGGER.info(f"Left guild {guild_id} voice channel.")
async def load_opus(self): async def load_opus(self):
# ... this method is unchanged ... if opus.is_loaded():
LOGGER.info("Opus library is already loaded.")
return
processor = platform.machine() processor = platform.machine()
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
LOGGER.debug(f"Processor: {processor}, OS: {os.name}")
LOGGER.debug(f"Attempting to load Opus. Processor: {processor}, OS: {os.name}")
try: try:
if os.name == 'nt': if os.name == 'nt':
if processor == "AMD64": if processor == "AMD64":
@@ -164,20 +159,11 @@ class DiscordBotManager:
LOGGER.info("Loaded OPUS library for armv7l") LOGGER.info("Loaded OPUS library for armv7l")
else: else:
opus.load_opus('libopus.so.0') opus.load_opus('libopus.so.0')
LOGGER.info(f"Loaded system OPUS library for {processor}") LOGGER.info(f"Attempted to load system OPUS library for {processor}")
except Exception as e: except Exception as e:
LOGGER.error(f"Failed to load OPUS library: {e}") LOGGER.error(f"Failed to load OPUS library: {e}")
raise RuntimeError("Could not load a valid Opus library. Voice functionality will fail.") raise RuntimeError("Could not load a valid Opus library. Voice functionality will fail.")
async def set_presence(self, system_name: str): if not opus.is_loaded():
# ... this method is unchanged ... raise RuntimeError("Opus library could not be loaded. Please ensure it is installed correctly.")
if not self.bot or not self.bot.is_ready():
LOGGER.warning("Bot is not ready, cannot set presence.")
return
try:
activity = Activity(type=ActivityType.listening, name=system_name)
await self.bot.change_presence(activity=activity)
LOGGER.info(f"Bot presence set to 'Listening to {system_name}'")
except Exception as pe:
LOGGER.error(f"Unable to set presence: '{pe}'")