From 3eb800ac7021c24cfbdcb0ed919fafc168f4fcdf Mon Sep 17 00:00:00 2001 From: Benjamyn Love Date: Sat, 20 Sep 2025 20:06:01 +1000 Subject: [PATCH] initial --- .gitignore | 9 ++++ audio_setup.bash | 35 +++++++++++++ config.py | 35 +++++++++++++ create_sound.py | 16 ++++++ example_request.json | 0 init.bash | 11 ++++ lookup_sounds.py | 104 +++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + soundboard | 3 ++ soundboard.py | 90 ++++++++++++++++++++++++++++++++ sounds/.gitkeep | 0 sounds/tina.json_old | 5 ++ static/imgs/button.png | Bin 0 -> 13210 bytes static/imgs/button_old.png | Bin 0 -> 13208 bytes stderr.txt | 10 ++++ stdout.txt | 2 + templates/settings.html | 55 ++++++++++++++++++++ templates/sounds.html | 75 ++++++++++++++++++++++++++ test.sh | 8 +++ 19 files changed, 460 insertions(+) create mode 100644 .gitignore create mode 100644 audio_setup.bash create mode 100644 config.py create mode 100644 create_sound.py create mode 100644 example_request.json create mode 100644 init.bash create mode 100644 lookup_sounds.py create mode 100644 requirements.txt create mode 100644 soundboard create mode 100644 soundboard.py create mode 100644 sounds/.gitkeep create mode 100644 sounds/tina.json_old create mode 100644 static/imgs/button.png create mode 100644 static/imgs/button_old.png create mode 100644 stderr.txt create mode 100644 stdout.txt create mode 100644 templates/settings.html create mode 100644 templates/sounds.html create mode 100644 test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c6141e --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vscode/ +venv/ +tmp/ +sounds/*.mp3 +sounds/*.json +*.pyc +*.m4a +*.opus +staging/ \ No newline at end of file diff --git a/audio_setup.bash b/audio_setup.bash new file mode 100644 index 0000000..3460f41 --- /dev/null +++ b/audio_setup.bash @@ -0,0 +1,35 @@ +#!/bin/bash + +# Benjamyn Love +# Soundboard setup +# Based on thread https://superuser.com/questions/1675877/how-to-create-a-new-pipewire-virtual-device-that-to-combines-an-real-input-and-o + +# VARIABLES +VIRTUAL_SINK_NAME="soundboard" +VIRTUAL_MIC_NAME="mic-soundboard" +PHYS_MIC_L="alsa_input.usb-R__DE_Microphones_R__DE_NT-USB_Mini_4B14FBA6-00.mono-fallback:capture_MONO" +PHYS_MIC_R="alsa_input.usb-R__DE_Microphones_R__DE_NT-USB_Mini_4B14FBA6-00.mono-fallback:capture_MONO" + +# Create a combined audio sink and soundboard-specific sink +pactl load-module module-null-sink media.class=Audio/Sink sink_name=${VIRTUAL_SINK_NAME} channel_map=stereo + +# Create a virtual microphone +pactl load-module module-null-sink media.class=Audio/Source/Virtual sink_name=${VIRTUAL_MIC_NAME} channel_map=front-left,front-right + +# Create loopback +#pactl load-module module-loopback + +# Link the loopback +# +#input.loopback-3441-13:input_FL +# |<- mic-soundboard:capture_FL +#input.loopback-3441-13:input_FR +# |<- mic-soundboard:capture_FR + +# Link the microphone to the virtual sink +pw-link ${PHYS_MIC_L} ${VIRTUAL_SINK_NAME}:playback_FL +pw-link ${PHYS_MIC_R} ${VIRTUAL_SINK_NAME}:playback_FR + +# Link the combined sink to the virtual microphone +pw-link ${VIRTUAL_SINK_NAME}:monitor_FL ${VIRTUAL_MIC_NAME}:input_FL +pw-link ${VIRTUAL_SINK_NAME}:monitor_FR ${VIRTUAL_MIC_NAME}:input_FR diff --git a/config.py b/config.py new file mode 100644 index 0000000..41bf872 --- /dev/null +++ b/config.py @@ -0,0 +1,35 @@ +import configparser +from pathlib import Path +from os import path + + +class Config: + def __init__(self, application_basedir, devicename, port, host): + self.application_basedir = application_basedir + self.devicename = devicename + self.port = port + self.host = host + self.current_volume = 0.6 + + @staticmethod + def load(filepath): + c = configparser.ConfigParser() + try: + c.read(path.expanduser(filepath)) + application_basedir = path.expanduser( + c.get("General", "application_basedir") + ) + host = c.get("Network", "host") + port = c.get("Network", "port") + devicename = c.get("Audio", "device_name") + + # Create directory if it doesn't exist + # along with the sounds directory + Path(application_basedir).mkdir(parents=True, exist_ok=True) + Path(f"{application_basedir}/sounds").mkdir(parents=True, exist_ok=True) + + config = Config(application_basedir, devicename, port, host) + return config + except Exception as e: + print(e) + return None diff --git a/create_sound.py b/create_sound.py new file mode 100644 index 0000000..f6d4760 --- /dev/null +++ b/create_sound.py @@ -0,0 +1,16 @@ +from lookup_sounds import Sound +from sys import argv + +try: + filename = argv[1] + name = argv[2] + route = argv[3] +except IndexError: + print(f"Invalid args, {argv[0]} filename name route") + exit(2) + +# Create new sound object +sound = Sound(filename, name, route) + +print(sound.json()) +sound.save() diff --git a/example_request.json b/example_request.json new file mode 100644 index 0000000..e69de29 diff --git a/init.bash b/init.bash new file mode 100644 index 0000000..a76f06d --- /dev/null +++ b/init.bash @@ -0,0 +1,11 @@ +#!/bin/bash + +# Setup the environment +cd ~/Documents/Projects/soundboard +source venv/bin/activate + +# Init the audio devices +bash audio_setup.bash + +# Start the soundboard application +python soundboard.py diff --git a/lookup_sounds.py b/lookup_sounds.py new file mode 100644 index 0000000..20127c0 --- /dev/null +++ b/lookup_sounds.py @@ -0,0 +1,104 @@ +import glob +import json +import magic +import tempfile +import shutil +from pathlib import Path +from config import Config +from base64 import b64decode + + +class Sounds: + def __init__(self): + self.sounds = list() + self.config = Config.load("~/.config/soundboard.ini") + + def load_sounds(self): + self.sounds = list() + files = glob.glob(f"{self.config.application_basedir}/sounds/*.json") + for sound in files: + with open(sound, "r") as f: + d = json.load(f) + s = Sound(d.get("filename"), d.get("name"), d.get("route")) + print(f"Registering sound: {s.name}") + self.sounds.append(s) + + def lookup(self, route): + for s in self.sounds: + if s.route == route: + return s + + return None + + def add(self, name, route, filename, mp3_file): + if self.lookup(route) is not None: + return (False, "Route already exists") + # Check if name clashes + json_path = f"{self.config.application_basedir}/sounds/{route}.json" + if Path(f"{json_path}").exists(): + return (False, f"JSON file exists: {route}.json") + # Check if the file already exists + mp3_filepath = f"{self.config.application_basedir}{filename}" + if Path(mp3_filepath).exists(): + return (False, f"MP3 file exists on disk: {filename}") + # Check if the file is actually an MP3 + try: + mp3_decoded = b64decode(mp3_file) + except Exception as e: + return (False, e) + + mp3_temp = tempfile.NamedTemporaryFile( + mode="w+b", + buffering=0, + encoding=None, + newline=None, + prefix="mp3tmp_", + dir="tmp", + ) + + mp3_temp.write(mp3_decoded) + + m = magic.open(magic.MAGIC_MIME) + m.load() + if m.file(mp3_temp.name) != "audio/mpeg": + return (False, "File is not of type mp3") + # Save the mp3 file to disk + shutil.copy2(mp3_temp.name, mp3_filepath) + mp3_temp.close() + # Generate the JSON file + # Save the JSON file to disk + with open(json_path, "w") as f: + json.dump({"name": name, "filename": filename, "route": route}, f) + + # Reload all the sounds + self.load_sounds() + return (True, "Successfully added sound") + + def list(self): + return "

".join([sound.name for sound in self.sounds]) + + +class Sound: + def __init__(self, filename, name, route): + self.filename = filename + self.name = name + self.route = route + + def json(self): + data = { + "filename": self.filename, + "name": self.name, + "route": self.route, + } + return data + + def save(self): + config = Config.load("~/.config/soundboard.ini") + path = Path(f"{config.application_basedir}/sounds/{self.route}.json") + if not path.exists(): + with open( + f"{config.application_basedir}/sounds/{self.route}.json", "w" + ) as f: + json.dump(self.json(), f) + else: + print(f"Sound already exists with name {self.route}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3785397 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +pygame diff --git a/soundboard b/soundboard new file mode 100644 index 0000000..9045751 --- /dev/null +++ b/soundboard @@ -0,0 +1,3 @@ +#!/bin/bash + +curl -q http://localhost:5000/$1 \ No newline at end of file diff --git a/soundboard.py b/soundboard.py new file mode 100644 index 0000000..5c94396 --- /dev/null +++ b/soundboard.py @@ -0,0 +1,90 @@ +from pygame import mixer +from pygame import error as pygame_error +from flask import Flask, render_template, request +from config import Config + +import lookup_sounds + +app = Flask(__name__) +config = Config.load("~/.config/soundboard.ini") + +try: + mixer.init(devicename="soundboard Audio/Sink sink") +except pygame_error: + print("Failed to bind to sink") + exit(1) + +sounds = lookup_sounds.Sounds() +sounds.load_sounds() + + +@app.route("/") +def index(): + return render_template("sounds.html", data=sounds.sounds) + + +@app.route("/") +def play(sound): + l_sound = sounds.lookup(sound) + channel = mixer.find_channel() + if channel is None: + return "Out of usable audio channels", 418 + if l_sound is None: + return "False", 404 + m_sound = mixer.Sound(f"{config.application_basedir}{l_sound.filename}") + channel.set_volume(config.current_volume) + + channel.play(m_sound) + return "True" + + +@app.route("/vol", methods=["GET", "POST"]) +def volume(): + match (request.method): + case "GET": + return str(config.current_volume) + case "POST": + try: + float(request.data) + config.current_volume = float(request.data) + return request.data + except Exception as e: + print(e) + return str(e) + case _: + return "Method not implemented", 418 + + +@app.route("/settings") +def settings(): + return render_template("settings.html", config=config) + + +@app.route("/new", methods=["POST"]) +def new_sound(): + match (request.method): + case "POST": + data = request.json + # Validate we have the required data to add a new sound + for field in ["name", "filename", "route", "file"]: + if data.get(field, None) is None: + return f"Bad data missing field {field}", 400 + + ret, message = sounds.add(**data) + print(ret) + if ret is False: + return f"Failed to add sound: {message}" + + return "Successfully Added the sound", 200 + case _: + return "Method not implemented", 418 + + +@app.route("/reload") +def reload(): + sounds.load_sounds() + return "True" + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0") diff --git a/sounds/.gitkeep b/sounds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sounds/tina.json_old b/sounds/tina.json_old new file mode 100644 index 0000000..bf5baf6 --- /dev/null +++ b/sounds/tina.json_old @@ -0,0 +1,5 @@ +{ + "name": "Tina come get some dinner", + "filename": "sounds/tina.mp3", + "route": "tina" +} diff --git a/static/imgs/button.png b/static/imgs/button.png new file mode 100644 index 0000000000000000000000000000000000000000..50fdb5025cfe5fd9ff6a2626bb75cf2fbcfc83e0 GIT binary patch literal 13210 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 + + + + + + Soundboard settings + + + + +

Settings:

+
+ +
+ + + + \ No newline at end of file diff --git a/templates/sounds.html b/templates/sounds.html new file mode 100644 index 0000000..e4c2c71 --- /dev/null +++ b/templates/sounds.html @@ -0,0 +1,75 @@ + + + + + + + Sounds + + + + +
+ {% for item in data %} +
+

{{item['name']}}

+ Play sound +
+ + {% endfor %} +
+ + + + \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..6e2cff8 --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +curl http://localhost:5000/$1 +curl http://localhost:5000/$1 +curl http://localhost:5000/$1 +curl http://localhost:5000/$1 +curl http://localhost:5000/$1 +curl http://localhost:5000/$1 +curl http://localhost:5000/$1 +curl http://localhost:5000/$1