Working board

This commit is contained in:
Jack O'Sullivan 2023-04-22 23:03:47 +01:00
parent e69e0e213c
commit 5d1eb7bc7a
6 changed files with 212 additions and 4 deletions

1
.envrc
View File

@ -1 +1,2 @@
nix_direnv_watch_file pyproject.toml poetry.lock
use flake

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
/result
/ideas.md
__pycache__/
/boardie.yaml
/sounds/

View File

@ -1,8 +1,161 @@
import shutil
import argparse
import os.path
import random
import evdev
import pyaudio
import pydub
import yaml
class Sound:
def __init__(self, combo, files, bit_depth=16, sample_rate=44100, channels=2):
keys = []
for k in combo.upper().split('-'):
match k:
case 'SHIFT':
k = 'LEFTSHIFT'
keys.append(evdev.ecodes.ecodes[f'KEY_{k}'])
self.held = set(keys[:-1])
self.key = keys[-1]
self.sounds = []
for fname in files:
self.sounds.append((
fname,
pydub.AudioSegment.from_file(fname)
.set_sample_width(bit_depth // 8)
.set_frame_rate(sample_rate)
.set_channels(channels)
.normalize()))
self.active = None
def stop(self):
self.active = None
def play(self):
name, self.active = random.choice(self.sounds)
print(f'Playing {name}')
def next_chunk(self, frame_count):
if not self.active:
return pydub.AudioSegment.silent(0)
chunk = self.active.get_sample_slice(end_sample=frame_count)
self.active = self.active.get_sample_slice(start_sample=frame_count)
if self.active.frame_count == 0:
self.active = None
return chunk
class Boardie:
bit_depth = 16
channels = 2
def __init__(self, config_file, device, audio_device=None):
self.config_file = config_file
self.sounds = []
self.device = evdev.InputDevice(device)
self.device.grab()
self.pa = pyaudio.PyAudio()
dev_info = self.pa.get_device_info_by_index(audio_device)
self.sample_rate = int(dev_info['defaultSampleRate'])
self.stream = self.pa.open(
output_device_index=audio_device,
format=self.pa.get_format_from_width(self.bit_depth // 8),
rate=self.sample_rate,
channels=self.channels,
output=True,
stream_callback=self._acallback)
self.reload()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
for s in self.sounds:
s.stop()
self.stream.close()
self.pa.terminate()
self.device.ungrab()
self.device.close()
def reload(self):
print('Loading config')
for s in self.sounds:
s.stop()
with open(self.config_file) as f:
data = yaml.full_load(f)
dir_ = data['dir']
self.sounds = []
for combo, fnames in data['sounds'].items():
if not isinstance(fnames, list):
fnames = [fnames]
files = list(map(lambda f: os.path.join(dir_, f), fnames))
self.sounds.append(Sound(
combo, files,
bit_depth=self.bit_depth, sample_rate=self.sample_rate, channels=self.channels))
def _acallback(self, _in_data, frame_count, time_info, status):
seg = pydub.AudioSegment(
data=frame_count * self.channels * (self.bit_depth // 2) * b'\0',
frame_rate=self.sample_rate,
sample_width=self.bit_depth // 8,
channels=self.channels)
for s in self.sounds:
seg = seg.overlay(s.next_chunk(frame_count))
return seg.raw_data, pyaudio.paContinue
def run(self):
for ev in self.device.read_loop():
if ev.type != evdev.ecodes.EV_KEY:
continue
ev = evdev.categorize(ev)
if ev.keystate != ev.key_down:
continue
held = set(self.device.active_keys()) & {evdev.ecodes.KEY_LEFTSHIFT, evdev.ecodes.KEY_LEFTCTRL}
if ev.scancode == evdev.ecodes.KEY_ESC:
if evdev.ecodes.KEY_LEFTSHIFT in held:
self.reload()
else:
for s in self.sounds:
s.stop()
continue
for s in self.sounds:
if ev.scancode == s.key and s.held == held:
s.play()
break
def main():
print(f'ffmpeg: {shutil.which("ffmpeg")}')
parser = argparse.ArgumentParser(
'boardie', description='Linux soundboard',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-a', '--audio-device', help='Audio device to use, pass ? to list')
parser.add_argument('-f', '--config', help='Config file', default='boardie.yaml')
parser.add_argument('device', help='Keyboard event device')
args = parser.parse_args()
if args.audio_device == '?':
pa = pyaudio.PyAudio()
apis = []
for i in range(pa.get_host_api_count()):
apis.append(pa.get_host_api_info_by_index(i)['name'])
for i in range(pa.get_device_count()):
info = pa.get_device_info_by_index(i)
if info['maxOutputChannels'] == 0:
continue
print(f"{info['index']}: ({apis[info['hostApi']]}) {info['name']}")
return
audio_dev = int(args.audio_device) if args.audio_device is not None else None
with Boardie(args.config, args.device, audio_device=audio_dev) as boardie:
boardie.run()

View File

@ -49,8 +49,9 @@
{
devShells.default = pkgs.devshell.mkShell {
packages = with pkgs; [
poetry
ffmpeg
poetry
(pkgs.poetry2nix.mkPoetryEnv {
projectDir = ./.;
overrides = pyOverrides pkgs;

52
poetry.lock generated
View File

@ -47,7 +47,57 @@ files = [
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
]
[[package]]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "a3a55972f5afbae28f3b04b8c2d51919bce6524c0afcf43afa8a5590691a5c06"
content-hash = "1d6c1451eb78fd670ce9f340757bd5bf486b5407da603f6ca2f19441451633fc"

View File

@ -10,6 +10,7 @@ python = "^3.10"
evdev = "^1.6.1"
pydub = "^0.25.1"
pyaudio = "^0.2.13"
pyyaml = "^6.0"
[tool.poetry.scripts]
boardie = 'boardie:main'