From 24adc47109088465b6d5202553a61112eb9fda1a Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 27 Mar 2022 00:50:42 -0400 Subject: [PATCH] Dan's Update - New classes for noisegate & audio stream - Ability to manipulate raw data in real-time --- BotResources.py | 2 +- NoiseGatev2.py | 151 +++++++++++++++++++++++++++++++++--------------- bot.py | 44 +++++++++----- sound.py | 100 -------------------------------- 4 files changed, 134 insertions(+), 163 deletions(-) delete mode 100644 sound.py diff --git a/BotResources.py b/BotResources.py index bcfab85..5761c8c 100644 --- a/BotResources.py +++ b/BotResources.py @@ -1,6 +1,6 @@ -import sound import configparser from os.path import exists +from NoiseGatev2 import AudioStream PDB_ACCEPTABLE_HANDLERS = {'gqrx': { 'Modes': ['wfm', 'fm'] diff --git a/NoiseGatev2.py b/NoiseGatev2.py index 7381b52..9655a6a 100644 --- a/NoiseGatev2.py +++ b/NoiseGatev2.py @@ -1,25 +1,24 @@ +import audioop +import math import pyaudio -import struct +import discord import numpy -import time -from threading import Thread -sound_buffer = [] +THRESHOLD = 50 +voice_connection = None -class AudioStream(Thread): - def __init__(self, _channels: int = 1, _sample_rate: int = 48000, _frames_per_buffer: int = 2048, +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): - super(AudioStream, self).__init__() + _output: bool = True, _init_on_startup: bool = True): self.paInstance_kwargs = { - 'format': pyaudio.paFloat32, + 'format': pyaudio.paInt16, 'channels': _channels, 'rate': _sample_rate, 'input': _input, 'output': _output, - 'frames_per_buffer': _frames_per_buffer, - 'stream_callback': callback + 'frames_per_buffer': _frames_per_buffer } if _input_device_index: @@ -36,15 +35,18 @@ class AudioStream(Thread): print(f"[AudioStream.__init__]:\tOutput was not enabled." f" Reinitialize with '_output=True'") - # Init PyAudio instance - self.paInstance = pyaudio.PyAudio() + if _init_on_startup: + # Init PyAudio instance + print("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 and _input_device_index: - self.init_stream() + # Define and initialize stream object if we have been passed a device ID (pyaudio.open) + self.stream = None - # temp section + if _output_device_index or _input_device_index: + if _init_on_startup: + print("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) @@ -64,7 +66,7 @@ class AudioStream(Thread): self.close_if_open() - # Reopen the stream + # Open the stream self.stream = self.paInstance.open(**self.paInstance_kwargs) def close_if_open(self): @@ -75,56 +77,109 @@ class AudioStream(Thread): self.stream.close() print(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.") - def list_devices(self, _show_input_devices: bool = True, _show_output_devices: bool = True): + def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True): 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: - if _show_input_devices: - print("Input Device id ", i, " - ", - self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')) + 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: + print("Input Device id ", i, " - ", input_device) if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0: - if _show_output_devices: - print("Output Device id ", i, " - ", - self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')) + 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: + print("Output Device id ", i, " - ", output_device) + return devices -class NoiseGate(AudioStream): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def run(self) -> None: - # Start the audio stream - self.stream.start_stream() - - global sound_buffer - # While the stream is running, display the stream buffer in floats? - while self.stream.is_active(): - if len(sound_buffer) > 0: - for buffer in sound_buffer: - volume_normalization = numpy.linalg.norm(numpy.fromstring(buffer)) * 10 - print(str(float(volume_normalization))) - - self.stream.stop_stream() + async def stop(self): + await voice_connection.disconnect() + self.close_if_open() self.stream.close() - self.paInstance.terminate() -def callback(in_data, frame_count, time_info, status): - global sound_buffer +class NoiseGate(AudioStream): + def __init__(self, _voice_connection, **kwargs): + super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs) + global voice_connection + voice_connection = _voice_connection + self.NGStream = NoiseGateStream(self) - sound_buffer.append(in_data) + def run(self) -> None: + global voice_connection + # Start the audio stream + self.stream.start_stream() + voice_connection.play(self.NGStream) - return in_data, pyaudio.paContinue + async def close(self): + await voice_connection.disconnect() + if self.stream.is_active: + self.stream.stop_stream() + +class NoiseGateStream(discord.AudioSource): + def __init__(self, _stream): + super(NoiseGateStream, self).__init__() + self.stream = _stream # The actual audio stream object + self.NG_fadeout = 480/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 in order to limit the prints + + 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 % 25 == 0: + print(f"{buffer_decibel} db") + + if buffer_decibel >= THRESHOLD: + self.NG_fadeout_count = self.NG_fadeout + self.process_set_count += 1 + + return bytes(curr_buffer) + + else: + if self.NG_fadeout_count > 0: + self.NG_fadeout_count -= 1 + print(f"Frames in fadeout remaining: {self.NG_fadeout_count}") + self.process_set_count += 1 + + return bytes(curr_buffer) + + except OSError as 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/bot.py b/bot.py index 5be70d8..1f080eb 100644 --- a/bot.py +++ b/bot.py @@ -3,8 +3,8 @@ import os import platform import discord import BotResources -import sound import configparser +import NoiseGatev2 from discord.ext import commands @@ -33,7 +33,9 @@ class Bot(commands.Bot): self.Bot_Connected = False # Init the audio devices list - self.Devices_List = sound.query_devices().items() + #self.Devices_List = sound.query_devices().items() + self.Devices_List = NoiseGatev2.AudioStream().list_devices(_display_input_devices=False, + _display_output_devices=False) # Init radio parameters self.profile_name = None @@ -79,6 +81,7 @@ class Bot(commands.Bot): brief="Joins the voice channel that the caller is in") async def join(ctx, *, member: discord.Member = None): member = member or ctx.author.display_name + print(f"Join requested by {member}") if not self.Bot_Connected: # Wait for the bot to be ready to connect @@ -89,45 +92,52 @@ class Bot(commands.Bot): if discord.opus.is_loaded(): channel = ctx.author.voice.channel + print("Sending hello") await ctx.send(f"Ok {str(member).capitalize()}, I'm joining {channel}") # Join the voice channel with the audio stream + print('Joining') voice_connection = await channel.connect() # Create an audio stream from selected device - self.streamHandler = sound.PCMStream(self.DEVICE_ID, voice_connection) - + print("Starting noisegate/stream handler") + self.streamHandler = NoiseGatev2.NoiseGate(_input_device_index=self.DEVICE_ID, + _voice_connection=voice_connection) # Start the audio stream - await self.streamHandler.play() + self.streamHandler.run() # Start the SDR and begin playing to the audio stream + print("Starting SDR") self.start_sdr() # Change the activity to the channel and band-type being used + print("Changing presence") await self.set_activity() # 'Lock' the bot from connecting + print("Locking the bot") self.Bot_Connected = True else: # Return that the opus library would not load await ctx.send("Opus won't load") + print("OPUS didn't load properly") else: await ctx.send(f"{str(member).capitalize()}, I'm already connected") + print("Bot is already in a channel") @self.command(help="Use this command to have the bot leave your channel", brief="Leaves the current voice channel") async def leave(ctx, member: discord.Member = None): member = member or ctx.author.display_name - if self.Bot_Connected: - print("Cleaning up") - # Stop the sound handlers - await self.streamHandler.pause() + print(f"Leave requested by {member}") - print("Disconnecting") + if self.Bot_Connected: + # Stop the sound handlers # Disconnect the client from the voice channel - await ctx.voice_client.disconnect() + print("Disconnecting") + await self.streamHandler.close() print("Changing presence") # Change the presence to away and '@ me' @@ -316,26 +326,29 @@ class Bot(commands.Bot): # Load the proper OPUS library for the device being used def load_opus(self): # Check the system type and load the correct library - # Linux ARM AARCH64 running 32bit OS if self.system_os_type == 'Linux_ARMv7l': + print(f"Loaded OPUS library for {self.system_os_type}") discord.opus.load_opus('./opus/libopus_armv7l.so') # Linux ARM AARCH64 running 64bit OS if self.system_os_type == 'Linux_AARCH64': + print(f"Loaded OPUS library for {self.system_os_type}") discord.opus.load_opus('./opus/libopus_aarcch64.so') # Windows 64bit if self.system_os_type == 'Windows_x64': + print(f"Loaded OPUS library for {self.system_os_type}") discord.opus.load_opus('./opus/libopus_amd64.dll') # Check to make sure the selected device is still available and has not changed its index def check_device(self, _override): # Check to see if an override has been passed if not _override: - for device, index in self.Devices_List: + print(f"Device list {self.Devices_List}") + for device, index in self.Devices_List['Input']: if int(index) == self.DEVICE_ID and str(device) == self.DEVICE_NAME: return True - for device, index in self.Devices_List: + for device, index in self.Devices_List['Input']: if str(device) == self.DEVICE_NAME: self.DEVICE_ID = int(index) return True @@ -376,11 +389,14 @@ class Bot(commands.Bot): if os.name == 'nt': if processor == "AMD64": self.system_os_type = 'Windows_x64' + print(f"OS/Arch is {self.system_os_type}") else: if processor == "aarch64": self.system_os_type = 'Linux_AARCH64' + print(f"OS/Arch is {self.system_os_type}") elif processor == "armv7l": self.system_os_type = 'Linux_ARMv7l' + print(f"OS/Arch is {self.system_os_type}") # Check to see if there is only one frequency def start_sdr(self): diff --git a/sound.py b/sound.py deleted file mode 100644 index 9bc76c3..0000000 --- a/sound.py +++ /dev/null @@ -1,100 +0,0 @@ -import threading -import time -import audioop -import discord -import sounddevice -import sounddevice as sd -from pprint import pformat - -DEFAULT = 0 -sd.default.channels = 2 -sd.default.dtype = "int16" -sd.default.latency = "low" -sd.default.samplerate = 48000 - - -class NoiseGate(threading.Thread): - def __init__(self, _voice_connection, _pcmstream_instance): - super().__init__() - self.voice_connection = _voice_connection - self.PCMStream_Instance = _pcmstream_instance - - def run(self) -> None: - while self.voice_connection.is_connected(): - print(f"Raw data: '{self.PCMStream_Instance.read(16)}'") - print(f"Volume: '{float(20 * audioop.rms(self.PCMStream_Instance.read(16), 2))}'") - if float(20 * audioop.rms(self.PCMStream_Instance.read(16), 2)) >= 5: - # Play the stream - self.voice_connection.play(discord.PCMAudio(self.PCMStream_Instance)) - while float(20 * audioop.rms(self.PCMStream_Instance.read(16), 2)) >= 5: - time.sleep(.5) - self.voice_connection.stop() - time.sleep(.1) - - -class PCMStream(NoiseGate): - globals() - def __init__(self, _device_id, _voice_connection): - super(PCMStream, self).__init__(_pcmstream_instance=self, _voice_connection=_voice_connection) - self.stream = sd.RawInputStream(device=_device_id) - - def change_device(self, _device_id): - self.clean_up() - self.stream = sd.RawInputStream(device=_device_id) - - # Stops and destroys the current stream - def clean_up(self): - if not self.stream.closed: - self.stream.stop(ignore_errors=True) - self.stream.close(ignore_errors=True) - - # Stops the current running stream but does not destroy it - async def pause(self): - if self.stream.active: - self.stream.stop(ignore_errors=True) - - # Plays the stream - async def play(self): - if not self.stream.active: - self.stream.start() - self.start() - - # call back read function for the stream - def read(self, num_bytes): - if self.stream.active: - # frame is 4 bytes - frames = int(num_bytes / 4) - data = self.stream.read(frames)[0] - # convert to pcm format - return bytes(data) - -class DeviceNotFoundError(Exception): - def __init__(self): - self.devices = sd.query_devices() - self.host_apis = sd.query_hostapis() - super().__init__("No Devices Found") - - def __str__(self): - return ( - f"Devices \n" - f"{self.devices} \n " - f"Host APIs \n" - f"{pformat(self.host_apis)}" - ) - - -def query_devices(): - options = { - device.get("name"): index - for index, device in enumerate(sd.query_devices()) - if (device.get("max_input_channels") > 0 and device.get("hostapi") == DEFAULT) - } - - if not options: - raise DeviceNotFoundError() - - return options - - - -