diff --git a/LICENSE b/LICENSE index 2071b23..67fe97b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) +Copyright (c) 2023 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 603a3f5..8d081a7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ # me.fier.aspew +Audio I/O for real-time applications, like VoIP. -Audio I/O for real time applications \ No newline at end of file +## Tools +- `aspew-in.py` takes a microphone as an input, and outputs it to pipe. +- `aspew-out.py` takes input from a pipe, and outputs it to a speaker. + +## Arguments +Arguments are pairs of key=value. + +### in & out +- `device`: The device, a number (Default: Your default audio device.) +- `format`: The encoding of your audio, choices: [https://people.csail.mit.edu/hubert/pyaudio/docs/#pasampleformat](https://people.csail.mit.edu/hubert/pyaudio/docs/#pasampleformat) (Default: `paUInt8`) +- `channels`: How many channels the audio has (Default: `1`) +- `bitrate`: How high the refresh rate of the audio is in Hz (Default: `8000`) +- `buffersize`: The buffer-size, in seconds. Higher buffer-sizes reduce CPU load and risk of stutter, but raise delay (Default: `0.05`) + +### out-only +- `store`: How much audio to store in the back-buffer at maximum before cutting it off, in seconds. Raise this if you get inconsistent playback (Default: 0.3s) + +## Examples +`./aspew-in.py | ./aspew-out.py` +Listen to your default microphone at default settings. + +`./aspew-in.py format=paInt16 bitrate=48000 channels=2 | ./aspew-out.py format=paInt16 bitrate=48000 channels=2` +Listen to your default microphone at 16-bit, 48000Hz, stereo. \ No newline at end of file diff --git a/aspew-in.py b/aspew-in.py new file mode 100755 index 0000000..ec602db --- /dev/null +++ b/aspew-in.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +import sys +import os +import time +import pyaudio +unbufferedStdout = os.fdopen(sys.stdout.fileno(),"wb",0) # Make unbuffered stdout + +def parseSettings(): + rtn = {} + for arg in sys.argv[1:]: + arg = arg.split("=",1) + if len(arg) < 2: arg.append("") + arg[0] = arg[0].strip(" \t\r\n") + arg[1] = arg[1].strip(" \t\r\n") + rtn[arg[0]] = arg[1] + return rtn + +def getSetting(lst,tp,keys,default = None): + for key in keys: + if key in lst: + return tp(lst[key]) + return default + +def streamHandler(in_data, frame_count, time_info, status): + unbufferedStdout.write(in_data) + return (in_data, pyaudio.paContinue) + +settings = parseSettings() +audioFormat = getSetting(settings,str,["f","format"],"paUInt8") +audioChannels = getSetting(settings,int,["c","channel","channels"],1) +audioRate = getSetting(settings,int,["r","rate","bitrate"],8000) +audioBuffer = getSetting(settings,float,["b","buffer","buffersize"],0.05) +audioDevice = getSetting(settings,int,["d","device"],None) + +audioFormat = getattr(pyaudio,audioFormat) +audioBuffer = round(audioRate * audioBuffer) + +kwargs = { + "input": True, + "input_device_index": audioDevice, + "format": audioFormat, + "channels": audioChannels, + "frames_per_buffer": audioBuffer, + "rate": audioRate, + "stream_callback": streamHandler +} + +audio = pyaudio.PyAudio() +stream = audio.open(**kwargs) + +try: + while stream.is_active(): + time.sleep(0.1) +except KeyboardInterrupt: + pass \ No newline at end of file diff --git a/aspew-out.py b/aspew-out.py new file mode 100755 index 0000000..fedc7ee --- /dev/null +++ b/aspew-out.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import sys +import os +import threading +import queue +import time +import math +import pyaudio +unbufferedStdout = os.fdopen(sys.stdout.fileno(),"wb",0) # Make unbuffered stdout +audioQueue = queue.Queue() + +lastTime = 0 +def legacyTimeMethod(): + currentTime = time.clock() + if lastTime == 0: + lastTime = currentTime + return 0 + + timeSpent = currentTime - lastTime + lastTime = currentTime + return timeSpent + +timeMethod = legacyTimeMethod +try: + timeMethod = time.perf_counter +except Exception: + pass + +class inThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + + def run(self): + try: + while True: + b = sys.stdin.buffer.read(audioFrameSize) + if b == b"": return + audioQueue.put(b) + except KeyboardInterrupt: + pass + +def getAudioFrame(): + global lastFrame + global audioStore + data = lastFrame + + #print(audioQueue.qsize()) + if audioQueue.qsize() > 0: + while audioQueue.qsize() > audioStore + 1: + data = audioQueue.get(False) + data = audioQueue.get(False) + + lastFrame = data + return data + +def streamHandler(in_data, frame_count, time_info, status): + data = b"" + data += getAudioFrame() + return (data, pyaudio.paContinue) + +def parseSettings(): + rtn = {} + for arg in sys.argv[1:]: + arg = arg.split("=",1) + if len(arg) < 2: arg.append("") + arg[0] = arg[0].strip(" \t\r\n") + arg[1] = arg[1].strip(" \t\r\n") + rtn[arg[0]] = arg[1] + return rtn + +def getSetting(lst,tp,keys,default = None): + for key in keys: + if key in lst: + return tp(lst[key]) + return default + +settings = parseSettings() +audioFormat = getSetting(settings,str,["f","format"],"paUInt8") +audioChannels = getSetting(settings,int,["c","channel","channels"],1) +audioRate = getSetting(settings,int,["r","rate","bitrate"],8000) +audioBuffer = getSetting(settings,float,["b","buffer","buffersize"],0.05) +audioStore = getSetting(settings,float,["s","store"],0.3) + +letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +audioBitrate = audioFormat +audioFormat = getattr(pyaudio,audioFormat) +for letter in letters: audioBitrate = audioBitrate.replace(letter,"") +audioBitrate = int(audioBitrate) +audioBuffer = round(audioRate * audioBuffer) +audioFrameSize = int(float(audioChannels) * (audioBitrate / 8) * audioBuffer) +audioStorePerSecond = audioBuffer / audioRate +audioStore = math.ceil(audioStore / audioStorePerSecond) +lastFrame = bytearray(int(audioFrameSize)) + +kwargs = { + "output": True, + "format": audioFormat, + "channels": audioChannels, + "frames_per_buffer": audioBuffer, + "rate": audioRate, + "stream_callback": streamHandler +} + +audio = pyaudio.PyAudio() +stream = audio.open(**kwargs) +inThread().start() +try: + while stream.is_active(): + time.sleep(0.1) +except KeyboardInterrupt: + pass \ No newline at end of file