diff --git a/Client/pdab/.gitignore b/Client/pdab/.gitignore new file mode 100644 index 0000000..9955cf2 --- /dev/null +++ b/Client/pdab/.gitignore @@ -0,0 +1,5 @@ +*venv/ +*__pycache__/ +*.html +*.exe +LICENSE \ No newline at end of file diff --git a/Client/pdab/NoiseGatev2.py b/Client/pdab/NoiseGatev2.py new file mode 100644 index 0000000..158937e --- /dev/null +++ b/Client/pdab/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/Client/pdab/getDevices.py b/Client/pdab/getDevices.py new file mode 100644 index 0000000..44964f5 --- /dev/null +++ b/Client/pdab/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/Client/pdab/main.py b/Client/pdab/main.py new file mode 100644 index 0000000..afec833 --- /dev/null +++ b/Client/pdab/main.py @@ -0,0 +1,75 @@ +import argparse, platform, os +from discord import Intents, Client, Member, opus +from discord.ext import commands +from NoiseGatev2 import NoiseGate + +# Load the proper OPUS library for the device being used +async def load_opus(): + # Check the system type and load the correct library + # Linux ARM AARCH64 running 32bit OS + processor = platform.machine() + print("Processor: ", processor) + if os.name == 'nt': + if processor == "AMD64": + print(f"Loaded OPUS library for AMD64") + opus.load_opus('./opus/libopus_amd64.dll') + return "AMD64" + else: + if processor == "aarch64": + print(f"Loaded OPUS library for aarch64") + opus.load_opus('./opus/libopus_aarcch64.so') + return "aarch64" + elif processor == "armv7l": + print(f"Loaded OPUS library for armv7l") + opus.load_opus('./opus/libopus_armv7l.so') + return "armv7l" + + +def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1): + intents = Intents.default() + + client = commands.Bot(command_prefix='!', 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) + + print("Loading opus") + await load_opus() + + if opus.is_loaded(): + print("Joining voice") + channelConnection = await channelIdToJoin.connect(timeout=60.0, reconnect=True) + print("Voice Connected") + 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 +) \ No newline at end of file diff --git a/Client/pdab/opus/libopus_aarcch64.so b/Client/pdab/opus/libopus_aarcch64.so new file mode 100644 index 0000000..8359d93 Binary files /dev/null and b/Client/pdab/opus/libopus_aarcch64.so differ diff --git a/Client/pdab/opus/libopus_amd64.dll b/Client/pdab/opus/libopus_amd64.dll new file mode 100644 index 0000000..74a8e35 Binary files /dev/null and b/Client/pdab/opus/libopus_amd64.dll differ diff --git a/Client/pdab/opus/libopus_armv7l.so b/Client/pdab/opus/libopus_armv7l.so new file mode 100644 index 0000000..7445645 Binary files /dev/null and b/Client/pdab/opus/libopus_armv7l.so differ diff --git a/Client/pdab/requirements.txt b/Client/pdab/requirements.txt new file mode 100644 index 0000000..40020b0 --- /dev/null +++ b/Client/pdab/requirements.txt @@ -0,0 +1,5 @@ +discord^=2.2.3 +PyNaCl^=1.5.0 +pyaudio^=0.2.13 +numpy^=1.24.3 +argparse \ No newline at end of file