If it aint broke dont fix it type shit

This commit is contained in:
Logan Cusano
2025-08-01 23:27:57 -04:00
parent 66e4e38e5e
commit a3c48cd651
2 changed files with 491 additions and 231 deletions

View File

@@ -1,98 +1,215 @@
import audioop import audioop
import logging
import math import math
import time
import pyaudio import pyaudio
import asyncio import discord
from internal.logger import create_logger import numpy
# You need to import the base AudioSource class from your specific library. voice_connection = None
# This is a common path, but yours might be different.
from discord import AudioSource
LOGGER = create_logger(__name__) LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2")
# Constants for audio processing
SAMPLES_PER_FRAME = 960
CHANNELS = 2
SAMPLE_RATE = 48000
FRAME_SIZE = SAMPLES_PER_FRAME * CHANNELS * 2 # 16-bit PCM
SILENT_FRAME = b'\x00' * FRAME_SIZE
class NoiseGateSource(AudioSource): # noinspection PyUnresolvedReferences
def __init__(self, audio_stream, threshold: int): class AudioStream:
self.audio_stream = audio_stream def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024,
self.threshold = threshold _input_device_index: int = None, _output_device_index: int = None, _input: bool = True,
self.ng_fadeout_count = 0 _output: bool = True, _init_on_startup: bool = True):
self.NG_FADEOUT_FRAMES = 12 # 240ms fadeout time self.paInstance_kwargs = {
'format': pyaudio.paInt16,
'channels': _channels,
'rate': _sample_rate,
'input': _input,
'output': _output,
'frames_per_buffer': _frames_per_buffer
}
def read(self) -> bytes: if _input_device_index:
""" if _input:
Reads data from the audio stream, applies the noise gate, self.paInstance_kwargs['input_device_index'] = _input_device_index
and returns a 20ms audio frame. else:
""" LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled."
try: f" Reinitialize with '_input=True'")
# 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 _output_device_index:
if len(pcm_data) != FRAME_SIZE: if _output:
return SILENT_FRAME self.paInstance_kwargs['output_device_index'] = _output_device_index
else:
LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled."
f" Reinitialize with '_output=True'")
# Calculate volume to check against the threshold. if _init_on_startup:
rms = audioop.rms(pcm_data, 2) # Init PyAudio instance
if rms == 0: LOGGER.info("Creating PyAudio instance")
# If there's no volume, check if we're in the fadeout period. self.paInstance = pyaudio.PyAudio()
if self.ng_fadeout_count > 0:
self.ng_fadeout_count -= 1
return pcm_data # Return the (silent) data to complete the fade
return SILENT_FRAME
db = 20 * math.log10(rms) # Define and initialize stream object if we have been passed a device ID (pyaudio.open)
self.stream = None
# If volume is above the threshold, send the audio and reset fadeout. if _output_device_index or _input_device_index:
if db >= self.threshold: if _init_on_startup:
self.ng_fadeout_count = self.NG_FADEOUT_FRAMES LOGGER.info("Init stream")
return pcm_data self.init_stream()
# If below threshold but still in the fadeout period, send the audio. def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None):
if self.ng_fadeout_count > 0: # Check what device was asked to be changed (or set)
self.ng_fadeout_count -= 1 if _new_input_device_index:
return pcm_data if self.paInstance_kwargs['input']:
self.paInstance_kwargs['input_device_index'] = _new_input_device_index
else:
LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized."
f" Reinitialize with '_input=True'")
# Otherwise, the gate is closed. Send silence. if _new_output_device_index:
return SILENT_FRAME if self.paInstance_kwargs['output']:
self.paInstance_kwargs['output_device_index'] = _new_output_device_index
else:
LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized."
f" Reinitialize with '_output=True'")
except Exception as e: self.close_if_open()
LOGGER.error(f"Error in NoiseGateSource.read: {e}", exc_info=True)
return SILENT_FRAME
def cleanup(self) -> None: # Open the stream
"""Called when the player stops.""" self.stream = self.paInstance.open(**self.paInstance_kwargs)
# The AudioStreamManager now handles cleanup.
LOGGER.info("Audio source cleanup called.")
pass
class AudioStreamManager: def close_if_open(self):
"""Manages the PyAudio instance and input stream.""" # Stop the stream if it is started
def __init__(self, input_device_index: int): if self.stream:
self.pa = pyaudio.PyAudio() if self.stream.is_active():
self.stream = self.pa.open( self.stream.stop_stream()
format=pyaudio.paInt16, self.stream.close()
channels=CHANNELS, LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.")
rate=SAMPLE_RATE,
input=True, def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
frames_per_buffer=SAMPLES_PER_FRAME, LOGGER.info('Getting a list of the devices connected')
input_device_index=input_device_index info = self.paInstance.get_host_api_info_by_index(0)
) numdevices = info.get('deviceCount')
devices = {
'Input': {},
'Output': {}
}
for i in range(0, numdevices):
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0:
input_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
devices['Input'][i] = input_device
if _display_input_devices:
LOGGER.debug(f"Input Device id {i} - {input_device}")
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0:
output_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
devices['Output'][i] = output_device
if _display_output_devices:
LOGGER.debug(f"Output Device id {i} - {output_device}")
return devices
async def stop(self):
await voice_connection.disconnect()
self.close_if_open()
self.stream.close()
self.paInstance.terminate()
# noinspection PyUnresolvedReferences
class NoiseGate(AudioStream):
def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs):
super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs)
global voice_connection
voice_connection = _voice_connection
self.THRESHOLD = _noise_gate_threshold
self.NGStream = NoiseGateStream(self)
self.Voice_Connection_Thread = None
def run(self) -> None:
global voice_connection
# Start the audio stream
LOGGER.debug(f"Starting stream")
self.stream.start_stream() self.stream.start_stream()
LOGGER.info(f"Audio stream started on device {input_device_index}") # Start the stream to discord
self.core()
def get_stream(self): def core(self, error=None):
return self.stream if error:
LOGGER.warning(error)
def terminate(self): while not voice_connection.is_connected():
if self.stream and self.stream.is_active(): time.sleep(.2)
if not voice_connection.is_playing():
LOGGER.debug(f"Playing stream to discord")
voice_connection.play(self.NGStream, after=self.core)
async def close(self):
LOGGER.debug(f"Closing")
await voice_connection.disconnect()
if self.stream.is_active:
self.stream.stop_stream() self.stream.stop_stream()
self.stream.close() LOGGER.debug(f"Stopping stream")
self.pa.terminate()
LOGGER.info("PyAudio instance terminated.")
# noinspection PyUnresolvedReferences
class NoiseGateStream(discord.AudioSource):
def __init__(self, _stream):
super(NoiseGateStream, self).__init__()
self.stream = _stream # The actual audio stream object
self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering
self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered
self.process_set_count = 0 # Counts how many processes have been made
def read(self):
try:
while voice_connection.is_connected():
curr_buffer = bytearray(self.stream.stream.read(960))
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:
if buffer_decibel >= self.stream.THRESHOLD:
LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db")
else:
LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db")
if buffer_decibel >= self.stream.THRESHOLD:
self.NG_fadeout_count = self.NG_fadeout
self.process_set_count += 1
if curr_buffer:
return bytes(curr_buffer)
else:
if self.NG_fadeout_count > 0:
self.NG_fadeout_count -= 1
LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}")
self.process_set_count += 1
if curr_buffer:
return bytes(curr_buffer)
except OSError as e:
LOGGER.warning(e)
pass
def audio_datalist_set_volume(self, datalist, volume):
""" Change value of list of audio chunks """
sound_level = (volume / 100.)
for i in range(len(datalist)):
chunk = numpy.fromstring(datalist[i], numpy.int16)
chunk = chunk * sound_level
datalist[i] = chunk.astype(numpy.int16)
if __name__ == '__main__':
input_index = int(input("Input:\t"))
output_index = int(input("Output:\t"))
ng = NoiseGate(_input_device_index=input_index, _output_device_index=output_index)
ng.list_devices()
ng.start()

View File

@@ -1,181 +1,324 @@
import asyncio import argparse
import platform import platform
import os import os
from discord import VoiceClient, VoiceChannel, opus, Activity, ActivityType, Intents import asyncio
import discord
from discord.ext import commands from discord.ext import commands
from typing import Optional, Dict from NoiseGatev2 import NoiseGate # Assuming NoiseGatev2.py is in the same directory
from internal.logger import create_logger
from internal.NoiseGatev2 import AudioStreamManager, NoiseGateSource
LOGGER = create_logger(__name__) # --- Opus Library Loading ---
def load_opus():
"""Loads the correct opus library for the operating system."""
try:
if os.name == 'nt':
processor = platform.machine()
if processor == "AMD64":
print("Loaded OPUS library for Windows AMD64")
discord.opus.load_opus('./opus/libopus_amd64.dll')
else:
print(f"Unsupported Windows processor: {processor}. Opus may not work.")
else:
processor = platform.machine()
print(f"Processor: {processor}")
if processor == "aarch64":
print("Loaded OPUS library for aarch64")
discord.opus.load_opus('./opus/libopus_aarcch64.so')
elif processor == "armv7l":
print("Loaded OPUS library for armv7l")
discord.opus.load_opus('./opus/libopus_armv7l.so')
else:
print(f"Attempting to load a generic opus library for {processor}")
discord.opus.load_opus('libopus.so.0')
intents = Intents.default() if discord.opus.is_loaded():
intents.voice_states = True print("Opus library loaded successfully.")
intents.guilds = True return True
else:
print("Opus library failed to load.")
return False
except Exception as e:
print(f"Error loading opus library: {e}")
return False
# --- Voice Cog for Multi-Server Management ---
class VoiceCog(commands.Cog):
"""Cog to handle all voice-related commands and state management."""
def __init__(self, bot, device_id, ng_threshold):
self.bot = bot
self.device_id = device_id
self.ng_threshold = ng_threshold
self.voice_states = {} # { guild_id: NoiseGate_instance }
@commands.Cog.listener()
async def on_ready(self):
print(f'Logged in as {self.bot.user} (ID: {self.bot.user.id})')
print('------')
# Internal API method to join a voice channel
async def internal_join_voice_channel(self, guild_id: int, channel_id: int):
guild = self.bot.get_guild(guild_id)
if not guild:
print(f"Guild with ID {guild_id} not found.")
return False
channel = guild.get_channel(channel_id)
if not channel or not isinstance(channel, discord.VoiceChannel):
print(f"Voice channel with ID {channel_id} not found in guild {guild.name}.")
return False
# Get voice client for this guild
voice_client = discord.utils.get(self.bot.voice_clients, guild=guild)
if voice_client:
if voice_client.channel == channel:
print(f"Already in channel: {channel.name} in guild {guild.name}.")
else:
await voice_client.move_to(channel)
print(f"Moved to channel: {channel.name} in guild {guild.name}.")
else:
try:
voice_client = await channel.connect(timeout=60.0, reconnect=True)
print(f"Connected to channel: {channel.name} in guild {guild.name}.")
except Exception as e:
print(f"Failed to connect to {channel.name} in guild {guild.name}. Error: {e}")
return False
if discord.opus.is_loaded():
# Create and start the NoiseGate audio stream for this server
stream_handler = NoiseGate(
_voice_connection=voice_client,
_noise_gate_threshold=self.ng_threshold,
_input_device_index=self.device_id
)
self.voice_states[guild.id] = stream_handler
stream_handler.run()
print(f"Started audio stream for server: {guild.name}")
return True
else:
print("Opus library not loaded. Cannot start audio stream.")
await voice_client.disconnect() # Disconnect if opus isn't loaded
return False
# Internal API method to leave a voice channel
async def internal_leave_voice_channel(self, guild_id: int):
if guild_id not in self.voice_states:
print(f"Not currently in a voice channel on guild ID {guild_id}.")
return False
guild = self.bot.get_guild(guild_id)
if not guild:
print(f"Guild with ID {guild_id} not found.")
return False
voice_client = discord.utils.get(self.bot.voice_clients, guild=guild)
if not voice_client:
print(f"Bot not in a voice channel in guild {guild.name}, but state exists. Cleaning up.")
del self.voice_states[guild.id]
return True
stream_handler = self.voice_states[guild.id]
await stream_handler.close() # Close the NoiseGate stream
del self.voice_states[guild.id]
print(f"Disconnected and stopped the audio stream for guild: {guild.name}")
return True
# Discord command for joining (for direct user interaction)
@commands.command(name='join')
async def join_command(self, ctx, *, channel: discord.VoiceChannel = None):
if not channel:
if ctx.author.voice:
channel = ctx.author.voice.channel
else:
await ctx.send("You are not connected to a voice channel. Please specify one to join.")
return
success = await self.internal_join_voice_channel(ctx.guild.id, channel.id)
if success:
await ctx.send(f"Connected to channel: {channel.name}.")
else:
await ctx.send(f"Failed to connect to {channel.name}.")
# Discord command for leaving (for direct user interaction)
@commands.command(name='leave')
async def leave_command(self, ctx):
success = await self.internal_leave_voice_channel(ctx.guild.id)
if success:
await ctx.send("Disconnected and stopped the audio stream.")
else:
await ctx.send("I am not currently in a voice channel on this server.")
@join_command.before_invoke
async def ensure_opus(self, ctx):
if not discord.opus.is_loaded():
await ctx.send("Opus audio library is not loaded. I cannot join a voice channel.")
raise commands.CommandError("Opus not loaded.")
# --- Discord Bot Manager Class ---
class DiscordBotManager: class DiscordBotManager:
def __init__(self): def __init__(self, client_id: str, device_id: int = 0, ng_threshold: int = 50):
self.bot: Optional[commands.Bot] = None self.client_id = client_id
self.bot_task: Optional[asyncio.Task] = None self.device_id = device_id
self.voice_connections: Dict[int, Dict] = {} self.ng_threshold = ng_threshold
self.token: Optional[str] = None self.bot = None
self.loop = asyncio.get_event_loop() self.voice_cog = None
self.lock = asyncio.Lock() self._bot_task = None # To hold the running bot task for graceful stopping
self._ready_event = asyncio.Event()
self._voice_ready_event = asyncio.Event()
async def start_bot(self, token: str): async def _setup_bot(self):
async with self.lock: # Define bot intents
if self.bot and not self.bot.is_closed(): intents = discord.Intents.default()
raise RuntimeError("Bot is already running.") intents.message_content = True # Required for commands
if self.bot_task and not self.bot_task.done(): intents.guilds = True # Required to get guild objects by ID
raise RuntimeError("Bot is already running.") intents.voice_states = True # Required to get voice channel info
self.token = token self.bot = commands.Bot(command_prefix='!', intents=intents)
self.bot = commands.Bot(command_prefix="!", intents=intents) self.voice_cog = VoiceCog(self.bot, self.device_id, self.ng_threshold)
await self.bot.add_cog(self.voice_cog)
@self.bot.event @self.bot.event
async def on_ready(): async def on_ready():
LOGGER.info(f'Logged in as {self.bot.user}') print(f'Bot fully ready: {self.bot.user}')
self._ready_event.set() # Set initial presence when the bot is ready
await self.set_presence("Broadcasting...", discord.Game)
@self.bot.event async def start_bot(self):
async def on_voice_state_update(member, before, after): if self.bot and self.bot.is_ready():
if member != self.bot.user: return print("Bot is already running.")
if before.channel is None and after.channel is not None: return
LOGGER.info(f"{member.name} joined voice channel {after.channel.name}")
self._voice_ready_event.set()
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}")
if not self._voice_ready_event.is_set(): self._voice_ready_event.set()
elif before.channel is not None and after.channel is None:
LOGGER.warning(f"{member.name} left voice channel {before.channel.name}")
# guild_id = before.channel.guild.id
# if guild_id in self.voice_connections:
# LOGGER.warning(f"Bot was disconnected from {guild_id} unexpectedly. Cleaning up...")
# await self.leave_voice_channel(guild_id)
# self._voice_ready_event.clear()
@self.bot.event if not load_opus():
async def on_disconnect(): print("Failed to load Opus library. Bot cannot start voice features.")
LOGGER.warning("Bot has been disconnected from Discord.") return
await self.load_opus() await self._setup_bot()
self.bot_task = self.loop.create_task(self.bot.start(token))
LOGGER.info("Waiting for bot to become ready...") print("Starting bot...")
try: try:
await asyncio.wait_for(self._ready_event.wait(), timeout=60.0) # Run the bot in a separate task so we can control it
LOGGER.info("Bot is ready.") self._bot_task = asyncio.create_task(self.bot.start(self.client_id))
except asyncio.TimeoutError: # Wait for the bot to connect (optional, useful for ensuring it's ready)
LOGGER.error("Timeout waiting for bot to become ready.") await self.bot.wait_until_ready()
if self.bot_task and not self.bot_task.done(): self.bot_task.cancel() print("Bot started and is ready.")
raise RuntimeError("Bot failed to become ready within timeout.") except discord.LoginFailure:
print("Failed to login to Discord. Check your client ID (token).")
except Exception as e:
print(f"An error occurred while starting the bot: {e}")
async def stop_bot(self): async def stop_bot(self):
async with self.lock: if not self.bot or not self.bot.is_ready():
if self.bot: print("Bot is not running or not ready.")
for guild_id in list(self.voice_connections.keys()): return
await self.leave_voice_channel(guild_id)
await self.bot.close()
self.bot = None
if self.bot_task:
self.bot_task.cancel()
self.bot_task = None
self.voice_connections.clear()
self._ready_event.clear()
LOGGER.info("Bot has been stopped.")
async def join_voice_channel(self, guild_id: int, channel_id: int, ng_threshold: int = 50, device_id: int = 4): print("Stopping bot...")
if not self.bot: raise RuntimeError("Bot is not running.") # Clean up all active voice connections before stopping
guild = self.bot.get_guild(guild_id) for guild_id in list(self.voice_cog.voice_states.keys()):
if not guild: raise ValueError("Guild not found.") await self.voice_cog.internal_leave_voice_channel(guild_id)
if not opus.is_loaded(): raise RuntimeError("Opus is not loaded.")
channel = guild.get_channel(channel_id)
if not isinstance(channel, VoiceChannel): raise ValueError("Channel is not a voice channel.")
if guild_id in self.voice_connections: raise RuntimeError("Already connected to this guild's voice channel.")
try: await self.bot.close()
self._voice_ready_event.clear() if self._bot_task:
voice_client = await channel.connect(timeout=60.0, reconnect=True) self._bot_task.cancel() # Cancel the bot's running task
await asyncio.wait_for(self._voice_ready_event.wait(), timeout=15.0) try:
await self._bot_task # Await cancellation to ensure cleanup
except asyncio.CancelledError:
pass
print("Bot stopped.")
self.bot = None # Reset bot instance
audio_manager = AudioStreamManager(input_device_index=device_id) async def join_voice_channel(self, guild_id: int, channel_id: int):
audio_source = NoiseGateSource(audio_manager.get_stream(), threshold=ng_threshold) if not self.bot or not self.bot.is_ready():
print("Bot is not running or ready. Cannot join voice channel.")
voice_client.play(audio_source, after=lambda e: LOGGER.error(f'Player error: {e}') if e else None) return False
return await self.voice_cog.internal_join_voice_channel(guild_id, channel_id)
self.voice_connections[guild_id] = {
"client": voice_client,
"audio_manager": audio_manager
}
LOGGER.info(f"Joined guild {guild_id} and started audio stream.")
except Exception as e:
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
async def leave_voice_channel(self, guild_id: int): async def leave_voice_channel(self, guild_id: int):
if not self.bot: raise RuntimeError("Bot is not running.")
connection_info = self.voice_connections.get(guild_id)
if not connection_info: raise RuntimeError("Not connected to the specified guild's voice channel.")
voice_client = connection_info.get("client")
if voice_client and voice_client.is_connected():
voice_client.stop()
await voice_client.disconnect()
audio_manager = connection_info.get("audio_manager")
if audio_manager:
audio_manager.terminate()
# Use pop to safely remove the key
self.voice_connections.pop(guild_id, None)
LOGGER.info(f"Left guild {guild_id} voice channel.")
async def load_opus(self):
if opus.is_loaded():
LOGGER.info("Opus library is already loaded.")
return
processor = platform.machine()
script_dir = os.path.dirname(os.path.abspath(__file__))
LOGGER.debug(f"Attempting to load Opus. Processor: {processor}, OS: {os.name}")
try:
if os.name == 'nt':
if processor == "AMD64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll'))
LOGGER.info("Loaded OPUS library for AMD64")
else:
if processor == "aarch64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so'))
LOGGER.info("Loaded OPUS library for aarch64")
elif processor == "armv7l":
opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so'))
LOGGER.info("Loaded OPUS library for armv7l")
else:
opus.load_opus('libopus.so.0')
LOGGER.info(f"Attempted to load system OPUS library for {processor}")
except Exception as e:
LOGGER.error(f"Failed to load OPUS library: {e}")
raise RuntimeError("Could not load a valid Opus library. Voice functionality will fail.")
if not opus.is_loaded():
raise RuntimeError("Opus library could not be loaded. Please ensure it is installed correctly.")
async def set_presence(self, system_name: str):
if not self.bot or not self.bot.is_ready(): if not self.bot or not self.bot.is_ready():
LOGGER.warning("Bot is not ready, cannot set presence.") print("Bot is not running or ready. Cannot leave voice channel.")
return False
return await self.voice_cog.internal_leave_voice_channel(guild_id)
async def set_presence(self, name: str, activity_type: discord.Activity = discord.ActivityType.listening):
if not self.bot or not self.bot.is_ready():
print("Bot is not running or ready. Cannot set presence.")
return return
try: try:
activity = Activity(type=ActivityType.listening, name=system_name) if activity_type == discord.Game:
activity = discord.Game(name=name)
elif activity_type == discord.Streaming:
activity = discord.Streaming(name=name, url="https://twitch.tv/your_stream_url_here") # Replace with actual URL
elif activity_type == discord.ActivityType.listening:
activity = discord.Activity(type=discord.ActivityType.listening, name=name)
elif activity_type == discord.ActivityType.watching:
activity = discord.Activity(type=discord.ActivityType.watching, name=name)
else:
print(f"Invalid activity type: {activity_type}. Defaulting to Game.")
activity = discord.Game(name=name)
await self.bot.change_presence(activity=activity) await self.bot.change_presence(activity=activity)
LOGGER.info(f"Bot presence set to 'Listening to {system_name}'") print(f"Presence set to: {name} ({activity_type.__name__})")
except Exception as pe: except Exception as e:
LOGGER.error(f"Unable to set presence: '{pe}'") print(f"Error setting presence: {e}")
# --- Example Usage / Main Entry Point ---
async def main_run():
parser = argparse.ArgumentParser(description="Discord Radio Bot Manager.")
parser.add_argument("clientId", type=str, help="The Discord bot's client token.")
parser.add_argument("deviceId", type=int, help="The ID of the audio input device to use.")
parser.add_argument("-n", "--NGThreshold", type=int, default=50, help="The noise gate threshold (default: 50).")
args = parser.parse_args()
# Instantiate the bot manager
bot_manager = DiscordBotManager(args.clientId, args.deviceId, args.NGThreshold)
# --- Start the bot ---
await bot_manager.start_bot()
# --- Example of how an external system (like your websocket) would interact ---
# You would replace this with your actual websocket logic
print("\nBot is running. You can now use internal_api calls or Discord commands.")
print("Example: Use !join <channel_id> in Discord.")
print("Example: Use !leave in Discord.")
print("Example: Simulating an external request to join a channel after 10 seconds...")
await asyncio.sleep(10) # Simulate delay
# Placeholder for guild_id and channel_id from your websocket server
# You would receive these from your websocket server
# For testing, you'll need to manually get a guild ID and channel ID where your bot is.
example_guild_id = 123456789012345678 # <<< REPLACE WITH AN ACTUAL GUILD ID
example_channel_id = 987654321098765432 # <<< REPLACE WITH AN ACTUAL VOICE CHANNEL ID
print(f"\nAttempting to join voice channel {example_channel_id} in guild {example_guild_id} via internal API...")
success_join = await bot_manager.join_voice_channel(example_guild_id, example_channel_id)
print(f"Internal join successful: {success_join}")
await asyncio.sleep(15) # Stay in channel for a bit
print("\nAttempting to leave voice channel via internal API...")
success_leave = await bot_manager.leave_voice_channel(example_guild_id)
print(f"Internal leave successful: {success_leave}")
await asyncio.sleep(5)
print("\nChanging bot presence to 'Streaming music'...")
await bot_manager.set_presence("Streaming music", discord.Streaming)
await asyncio.sleep(5)
print("\nChanging bot presence back to 'Idle'...")
await bot_manager.set_presence("Idle", discord.Game)
# Keep the bot running indefinitely until an external stop command or script exit
try:
while True:
await asyncio.sleep(3600) # Sleep for an hour, or until interrupted
except KeyboardInterrupt:
print("\nKeyboardInterrupt detected. Stopping bot...")
finally:
await bot_manager.stop_bot()
if __name__ == "__main__":
asyncio.run(main_run())