From 6528bd22aa6d3d2f4d3c67fbda81455781481ef7 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Wed, 17 May 2023 22:55:45 -0400 Subject: [PATCH] INIT Node Controlled Python Bot --- .gitignore | 2 + NoiseGatev2.py | 215 +++++++++++++++++++++++++++++++++++++ getDevices.py | 11 ++ main.py | 274 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 + 5 files changed, 507 insertions(+) create mode 100644 .gitignore create mode 100644 NoiseGatev2.py create mode 100644 getDevices.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9671d9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*venv/ +*__pycache__/ \ No newline at end of file diff --git a/NoiseGatev2.py b/NoiseGatev2.py new file mode 100644 index 0000000..158937e --- /dev/null +++ b/NoiseGatev2.py @@ -0,0 +1,215 @@ +import audioop +import logging +import math +import time + +import pyaudio +import discord +import numpy + +voice_connection = None + +LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2") + + +# noinspection PyUnresolvedReferences +class AudioStream: + def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024, + _input_device_index: int = None, _output_device_index: int = None, _input: bool = True, + _output: bool = True, _init_on_startup: bool = True): + self.paInstance_kwargs = { + 'format': pyaudio.paInt16, + 'channels': _channels, + 'rate': _sample_rate, + 'input': _input, + 'output': _output, + 'frames_per_buffer': _frames_per_buffer + } + + if _input_device_index: + if _input: + self.paInstance_kwargs['input_device_index'] = _input_device_index + else: + LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled." + f" Reinitialize with '_input=True'") + + if _output_device_index: + if _output: + self.paInstance_kwargs['output_device_index'] = _output_device_index + else: + LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled." + f" Reinitialize with '_output=True'") + + if _init_on_startup: + # Init PyAudio instance + LOGGER.info("Creating PyAudio instance") + self.paInstance = pyaudio.PyAudio() + + # Define and initialize stream object if we have been passed a device ID (pyaudio.open) + self.stream = None + + if _output_device_index or _input_device_index: + if _init_on_startup: + LOGGER.info("Init stream") + self.init_stream() + + def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None): + # Check what device was asked to be changed (or set) + if _new_input_device_index: + 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'") + + if _new_output_device_index: + 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'") + + self.close_if_open() + + # Open the stream + self.stream = self.paInstance.open(**self.paInstance_kwargs) + + def close_if_open(self): + # Stop the stream if it is started + if self.stream: + if self.stream.is_active(): + self.stream.stop_stream() + self.stream.close() + LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.") + + 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): + 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() + # Start the stream to discord + self.core() + + def core(self, error=None): + if error: + LOGGER.warning(error) + + while not voice_connection.is_connected(): + 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() + LOGGER.debug(f"Stopping stream") + + +# 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() diff --git a/getDevices.py b/getDevices.py new file mode 100644 index 0000000..44964f5 --- /dev/null +++ b/getDevices.py @@ -0,0 +1,11 @@ +from NoiseGatev2 import AudioStream + +print('Getting a list of devices') +list_of_devices = AudioStream().list_devices() +print("----- INPUT DEVICES -----") +for inputDevice in list_of_devices['Input']: + print(f"{inputDevice}\t-\t{list_of_devices['Input'][inputDevice]}") + +print("----- OUTPUT DEVICES -----") +for outputDevice in list_of_devices['Output']: + print(f"{outputDevice}\t-\t{list_of_devices['Output'][outputDevice]}") \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ccbf670 --- /dev/null +++ b/main.py @@ -0,0 +1,274 @@ +import argparse +from discord import Intents, Client, Member +from discord.ext import commands +from NoiseGatev2 import NoiseGate + +def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1): + intents = Intents.default() + intents.message_content = True + + client = Client(intents=intents) + + @client.event + async def on_ready(): + print(f'We have logged in as {client.user}') + + channelIdToJoin = client.get_channel(channelId) + print("Channel", channelIdToJoin) + channelConnection = await channelIdToJoin.connect() + print("Joined voice") + streamHandler = NoiseGate( + _input_device_index=deviceId, + _voice_connection=channelConnection, + _noise_gate_threshold=NGThreshold) + # Start the audio stream + streamHandler.run() + print("stream running") + + + client.run(clientId) + +parser = argparse.ArgumentParser() +parser.add_argument("deviceId", type=int, help="The ID of the audio device to use") +parser.add_argument("channelId", type=int, help="The ID of the voice channel to use") +parser.add_argument("clientId", type=str, help="The discord client ID") +parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50") +args = parser.parse_args() + +if (not args.NGThreshold): + args.NGThreshold = 50 + +print("Arguments:", args) + +main( + clientId=args.clientId, + channelId=args.channelId, + NGThreshold=args.NGThreshold, + deviceId=args.deviceId +) + + + + + + + + + + + + + + +#import asyncio +#import functools +#import itertools +#import math +#import random +# +#import discord +#from async_timeout import timeout +#from discord.ext import commands +# +# +#class VoiceError(Exception): +# pass +# +# +#class VoiceState: +# def __init__(self, bot: commands.Bot, ctx: commands.Context): +# self.bot = bot +# self._ctx = ctx +# +# self.current = None +# self.voice = None +# self.next = asyncio.Event() +# self.songs = SongQueue() +# +# self._loop = False +# self._volume = 0.5 +# self.skip_votes = set() +# +# self.audio_player = bot.loop.create_task(self.audio_player_task()) +# +# def __del__(self): +# self.audio_player.cancel() +# +# @property +# def loop(self): +# return self._loop +# +# @loop.setter +# def loop(self, value: bool): +# self._loop = value +# +# @property +# def volume(self): +# return self._volume +# +# @volume.setter +# def volume(self, value: float): +# self._volume = value +# +# @property +# def is_playing(self): +# return self.voice and self.current +# +# async def audio_player_task(self): +# while True: +# self.next.clear() +# +# if not self.loop: +# # Try to get the next song within 3 minutes. +# # If no song will be added to the queue in time, +# # the player will disconnect due to performance +# # reasons. +# try: +# async with timeout(180): # 3 minutes +# self.current = await self.songs.get() +# except asyncio.TimeoutError: +# self.bot.loop.create_task(self.stop()) +# return +# +# self.current.source.volume = self._volume +# self.voice.play(self.current.source, after=self.play_next_song) +# await self.current.source.channel.send(embed=self.current.create_embed()) +# +# await self.next.wait() +# +# def play_next_song(self, error=None): +# if error: +# raise VoiceError(str(error)) +# +# self.next.set() +# +# def skip(self): +# self.skip_votes.clear() +# +# if self.is_playing: +# self.voice.stop() +# +# async def stop(self): +# self.songs.clear() +# +# if self.voice: +# await self.voice.disconnect() +# self.voice = None +# +# +#class Music(commands.Cog): +# def __init__(self, bot: commands.Bot): +# self.bot = bot +# self.voice_states = {} +# +# def get_voice_state(self, ctx: commands.Context): +# state = self.voice_states.get(ctx.guild.id) +# if not state: +# state = VoiceState(self.bot, ctx) +# self.voice_states[ctx.guild.id] = state +# +# return state +# +# def cog_unload(self): +# for state in self.voice_states.values(): +# self.bot.loop.create_task(state.stop()) +# +# def cog_check(self, ctx: commands.Context): +# if not ctx.guild: +# raise commands.NoPrivateMessage('This command can\'t be used in DM channels.') +# +# return True +# +# async def cog_before_invoke(self, ctx: commands.Context): +# ctx.voice_state = self.get_voice_state(ctx) +# +# async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError): +# await ctx.send('An error occurred: {}'.format(str(error))) +# +# @commands.command(name='join', invoke_without_subcommand=True) +# async def _join(self, ctx: commands.Context): +# """Joins a voice channel.""" +# +# destination = ctx.author.voice.channel +# if ctx.voice_state.voice: +# await ctx.voice_state.voice.move_to(destination) +# return +# +# ctx.voice_state.voice = await destination.connect() +# +# @commands.command(name='summon') +# @commands.has_permissions(manage_guild=True) +# async def _summon(self, ctx: commands.Context, *, channel: discord.VoiceChannel = None): +# """Summons the bot to a voice channel. +# +# If no channel was specified, it joins your channel. +# """ +# +# if not channel and not ctx.author.voice: +# raise VoiceError('You are neither connected to a voice channel nor specified a channel to join.') +# +# destination = channel or ctx.author.voice.channel +# if ctx.voice_state.voice: +# await ctx.voice_state.voice.move_to(destination) +# return +# +# ctx.voice_state.voice = await destination.connect() +# +# @commands.command(name='leave', aliases=['disconnect']) +# @commands.has_permissions(manage_guild=True) +# async def _leave(self, ctx: commands.Context): +# """Clears the queue and leaves the voice channel.""" +# +# if not ctx.voice_state.voice: +# return await ctx.send('Not connected to any voice channel.') +# +# await ctx.voice_state.stop() +# del self.voice_states[ctx.guild.id] +# +# @commands.command(name='play') +# async def _play(self, ctx: commands.Context, *, search: str): +# """Plays a song. +# +# If there are songs in the queue, this will be queued until the +# other songs finished playing. +# +# This command automatically searches from various sites if no URL is provided. +# A list of these sites can be found here: https://rg3.github.io/youtube-dl/supportedsites.html +# """ +# +# if not ctx.voice_state.voice: +# await ctx.invoke(self._join) +# +# async with ctx.typing(): +# try: +# source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop) +# except YTDLError as e: +# await ctx.send('An error occurred while processing this request: {}'.format(str(e))) +# else: +# song = Song(source) +# +# await ctx.voice_state.songs.put(song) +# await ctx.send('Enqueued {}'.format(str(source))) +# +# @_join.before_invoke +# @_play.before_invoke +# async def ensure_voice_state(self, ctx: commands.Context): +# if not ctx.author.voice or not ctx.author.voice.channel: +# raise commands.CommandError('You are not connected to any voice channel.') +# +# if ctx.voice_client: +# if ctx.voice_client.channel != ctx.author.voice.channel: +# raise commands.CommandError('Bot is already in a voice channel.') +# +#intents = discord.Intents.default() +#intents.message_content = True +# +#bot = commands.Bot('music.', description="Brent's Revenge", intents=intents) +#bot.add_cog(Music(bot)) +# +# +#@bot.event +#async def on_ready(): +# print('Logged in as:\n{0.user.name}\n{0.user.id}'.format(bot)) +# +#bot.run('OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..382aa5b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +discord +PyNaCl +pyaudio +numpy +argparse \ No newline at end of file