181 lines
6.4 KiB
Python
181 lines
6.4 KiB
Python
import pyaudio
|
|
import wave, logging, threading, time, queue, signal, argparse, audioop
|
|
from os import path, makedirs
|
|
|
|
logging.basicConfig(format="%(asctime)s: %(message)s", level=logging.INFO,datefmt="%H:%M:%S")
|
|
|
|
class DiscordRecorder:
|
|
def __init__(self, DEVICE_ID, CHUNK = 960, FORMAT = pyaudio.paInt16, CHANNELS = 2, RATE = 48000, FILENAME = "./recs/radio.wav"):
|
|
self.pa_instance = pyaudio.PyAudio()
|
|
|
|
self.DEVICE_ID = DEVICE_ID
|
|
self.CHUNK = CHUNK
|
|
self.FORMAT = FORMAT
|
|
self.CHANNELS = CHANNELS
|
|
self.RATE = RATE
|
|
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
|
|
|
|
self.FILENAME = FILENAME
|
|
self._check_file_path_exists()
|
|
|
|
self.queued_frames = queue.Queue()
|
|
|
|
self.stop_threads = threading.Event()
|
|
|
|
self.recording_thread = None
|
|
self.saving_thread = None
|
|
|
|
self.running_stream = None
|
|
|
|
# Wrapper to check if the given filepath (not file itself) exists
|
|
def _check_file_path_exists(self):
|
|
if not path.exists(path.dirname(self.FILENAME)):
|
|
makedirs(path.dirname(self.FILENAME), exist_ok=True)
|
|
|
|
# Wrapper for the recorder thread; Adds new data to the queue
|
|
def _recorder(self):
|
|
logging.info("* Recording Thread Starting")
|
|
while True:
|
|
try:
|
|
curr_buffer = bytearray(self.stream.stream.read(self.CHUNK))
|
|
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 self.queued_frames.put(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 self.queued_frames.put(curr_buffer)
|
|
|
|
except OSError as e:
|
|
LOGGER.warning(e)
|
|
pass
|
|
|
|
# check for stop
|
|
if self.stop_threads.is_set():
|
|
break
|
|
|
|
# Wrapper for saver thread; Saves the queue to the file
|
|
def _saver(self):
|
|
logging.info("* Saving Thread Starting")
|
|
while True:
|
|
if not self.queued_frames.empty():
|
|
dequeued_frames = []
|
|
for i in range(self.queued_frames.qsize()):
|
|
dequeued_frames.append(self.queued_frames.get())
|
|
|
|
if not path.isfile(self.FILENAME):
|
|
wf = wave.open(self.FILENAME, 'wb')
|
|
wf.setnchannels(self.CHANNELS)
|
|
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
|
|
wf.setframerate(self.RATE)
|
|
wf.writeframes(b''.join(dequeued_frames))
|
|
wf.close()
|
|
else:
|
|
read_file = wave.open(self.FILENAME, 'rb')
|
|
read_file_data = read_file.readframes(read_file.getnframes())
|
|
read_file.close()
|
|
|
|
wf = wave.open(self.FILENAME, 'wb')
|
|
wf.setnchannels(self.CHANNELS)
|
|
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
|
|
wf.setframerate(self.RATE)
|
|
|
|
wf.writeframes(read_file_data)
|
|
wf.writeframes(b''.join(dequeued_frames))
|
|
wf.close()
|
|
|
|
# check for stop
|
|
if self.stop_threads.is_set():
|
|
break
|
|
|
|
time.sleep(5)
|
|
|
|
# Start the recording function
|
|
def start_recording(self):
|
|
logging.info("* Recording")
|
|
|
|
self.running_stream = self.pa_instance.open(
|
|
input_device_index=self.DEVICE_ID,
|
|
format=self.FORMAT,
|
|
channels=self.CHANNELS,
|
|
rate=self.RATE,
|
|
input=True,
|
|
frames_per_buffer=self.CHUNK
|
|
)
|
|
|
|
self.recording_thread = threading.Thread(target=self._recorder)
|
|
self.recording_thread.start()
|
|
|
|
self.saving_thread = threading.Thread(target=self._saver)
|
|
self.saving_thread.start()
|
|
|
|
# Stop the recording function
|
|
def stop_recording(self):
|
|
self.stop_threads.set()
|
|
self.recording_thread.join()
|
|
self.saving_thread.join()
|
|
self.running_stream.stop_stream()
|
|
self.running_stream.close()
|
|
self.pa_instance.terminate()
|
|
|
|
logging.info("* Done recording")
|
|
|
|
|
|
class GracefulExitCatcher:
|
|
def __init__(self, stop_callback):
|
|
self.stop = False
|
|
|
|
# The function to run when the exit signal is caught
|
|
self.stop_callback = stop_callback
|
|
|
|
# Update what happens when these signals are caught
|
|
signal.signal(signal.SIGINT, self.exit_gracefully)
|
|
signal.signal(signal.SIGTERM, self.exit_gracefully)
|
|
|
|
def exit_gracefully(self, *args):
|
|
logging.info("* Stop signal caught...")
|
|
|
|
# Stop the main loop
|
|
self.stop = True
|
|
|
|
# Run the given callback function
|
|
self.stop_callback()
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
|
|
parser.add_argument("filename", type=str, help="The filepath/filename of the output file")
|
|
args = parser.parse_args()
|
|
|
|
logging.debug("Arguments:", args)
|
|
|
|
recorder = DiscordRecorder(args.deviceId, FILENAME=args.filename)
|
|
|
|
exit_catcher = GracefulExitCatcher(recorder.stop_recording)
|
|
|
|
recorder.start_recording()
|
|
|
|
while not exit_catcher.stop:
|
|
time.sleep(1)
|