INIT Node Controlled Python Bot

This commit is contained in:
Logan Cusano
2023-05-17 22:55:45 -04:00
commit 6528bd22aa
5 changed files with 507 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*venv/
*__pycache__/

215
NoiseGatev2.py Normal file
View File

@@ -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()

11
getDevices.py Normal file
View File

@@ -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]}")

274
main.py Normal file
View File

@@ -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')

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
discord
PyNaCl
pyaudio
numpy
argparse