Working board
This commit is contained in:
parent
e69e0e213c
commit
5d1eb7bc7a
1
.envrc
1
.envrc
@ -1 +1,2 @@
|
|||||||
|
nix_direnv_watch_file pyproject.toml poetry.lock
|
||||||
use flake
|
use flake
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@
|
|||||||
/result
|
/result
|
||||||
/ideas.md
|
/ideas.md
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
/boardie.yaml
|
||||||
|
/sounds/
|
||||||
|
@ -1,8 +1,161 @@
|
|||||||
import shutil
|
import argparse
|
||||||
|
import os.path
|
||||||
|
import random
|
||||||
|
|
||||||
import evdev
|
import evdev
|
||||||
import pyaudio
|
import pyaudio
|
||||||
import pydub
|
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():
|
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()
|
||||||
|
@ -49,8 +49,9 @@
|
|||||||
{
|
{
|
||||||
devShells.default = pkgs.devshell.mkShell {
|
devShells.default = pkgs.devshell.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
poetry
|
|
||||||
ffmpeg
|
ffmpeg
|
||||||
|
|
||||||
|
poetry
|
||||||
(pkgs.poetry2nix.mkPoetryEnv {
|
(pkgs.poetry2nix.mkPoetryEnv {
|
||||||
projectDir = ./.;
|
projectDir = ./.;
|
||||||
overrides = pyOverrides pkgs;
|
overrides = pyOverrides pkgs;
|
||||||
|
52
poetry.lock
generated
52
poetry.lock
generated
@ -47,7 +47,57 @@ files = [
|
|||||||
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
{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]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "a3a55972f5afbae28f3b04b8c2d51919bce6524c0afcf43afa8a5590691a5c06"
|
content-hash = "1d6c1451eb78fd670ce9f340757bd5bf486b5407da603f6ca2f19441451633fc"
|
||||||
|
@ -10,6 +10,7 @@ python = "^3.10"
|
|||||||
evdev = "^1.6.1"
|
evdev = "^1.6.1"
|
||||||
pydub = "^0.25.1"
|
pydub = "^0.25.1"
|
||||||
pyaudio = "^0.2.13"
|
pyaudio = "^0.2.13"
|
||||||
|
pyyaml = "^6.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
boardie = 'boardie:main'
|
boardie = 'boardie:main'
|
||||||
|
Loading…
Reference in New Issue
Block a user