Skip to content

Commit

Permalink
Release version 0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
homeworkprod committed Oct 9, 2021
1 parent e143046 commit c5b347e
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changelog


## Version 0.1 (2021-10-09)

- First public release
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "scanner-soundboard"
version = "0.1.0"
authors = ["Jochen Kupperschmidt"]
edition = "2018"
description = "Trigger sounds via RFID tags or barcodes"
readme = "README.md"
homepage = "https://homework.nwsnet.de/releases/9b23/#scanner-soundboard"
repository = "https://github.com/homeworkprod/scanner-soundboard"
license = "MIT"
keywords = ["audio", "barcode", "rfid"]
categories = ["command-line-utilities", "multimedia::audio"]

[dependencies]
anyhow = "1.0"
clap = { version = "2.33.3", default-features = false }
evdev = { version = "0.11.1" }
rodio = { version = "0.14", default-features = false, features = ["mp3", "vorbis"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.5.8"
30 changes: 30 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Copyright (c) 2021, Jochen Kupperschmidt

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Scanner Soundboard

Reads codes via RFID or 1D/2D barcode USB scanners and plays soundfiles
mapped to them.

The input device is grabbed exlusively so that scanned codes will be
passed to the program regardless of what program/window currently has
focus.

I originally developed this to play insider jokes as custom sounds
(generated via text-to-speech engines) during regular internal evenings
of [Among Us](https://www.innersloth.com/games/among-us/) games. The
sounds are triggered by placing 3D-printed Among Us figurines (glued to
credit card-size RFID tags) on a cheap (~12 €) USB RFID reader, itself
covered by a 3D-printed plan of a map from the game.


## Usage

1. Have a bunch of sound files.

2. Have a bunch of codes to trigger the sounds. Those codes can come
from RFID tags (10-digit strings seem to be common) or whatever you
can fit in a 1D barcode or matrix/2D barcode (Aztec Code, Data
Matrix, QR code, etc.). Anything your scanner supports.

3. Specify the path of the sound files and map the codes to sound
filenames in a configuration file (see `config-example.toml` for an
example).

4. Find out where your scanner is available as a device. `sudo lsinput`
and `sudo dmesg | tail` can help you here. Note that the path can
change over time, depending on the order devices are connected.

5. Run the program, pointing to the configuration file and input device:

```sh
$ scanner-soundboard -c config.toml -i /dev/input/event23
```


## Sound Formats

Ogg Vorbis and MP3 are supported out of the box. However, the employed
audio playback library ([rodio](https://github.com/RustAudio/rodio))
also supports FLAC, WAV, MP4 and AAC, but those have to be enabled as
features in `Cargo.toml` and require recompilation of the program.


## License

Scanner Soundboard is licensed under the MIT license.
6 changes: 6 additions & 0 deletions config-example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sounds_path = "sounds"

[inputs_to_filenames]
"0000000001" = "sound001.ogg"
"0000000002" = "sound002.ogg"
"0000000003" = "sound003.ogg"
151 changes: 151 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2021 Jochen Kupperschmidt
* License: MIT (see file `LICENSE` for details)
*/

use anyhow::Result;
use clap::{crate_authors, crate_version, App, Arg, ArgMatches};
use evdev::{Device, EventType, InputEventKind, Key};
use rodio::{Decoder, OutputStream, Sink};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs::{read_to_string, File};
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process::exit;

#[derive(Deserialize)]
struct Config {
sounds_path: PathBuf,
inputs_to_filenames: HashMap<String, String>,
}

fn parse_args() -> ArgMatches<'static> {
App::new("RFID Soundboard")
.author(crate_authors!())
.version(crate_version!())
.arg(
Arg::with_name("config")
.short("c")
.long("config")
.help("Specify configuration file (e.g. `config.toml`)")
.required(true)
.takes_value(true)
.value_name("FILE"),
)
.arg(
Arg::with_name("input_device")
.short("i")
.long("input-device")
.help("Specify input device (e.g. `/dev/input/event23`)")
.required(true)
.takes_value(true)
.value_name("DEVICE"),
)
.get_matches()
}

fn load_config(path: &Path) -> Result<Config> {
let text = read_to_string(path)?;
let config: Config = toml::from_str(&text)?;
Ok(config)
}

fn get_char(key: Key) -> Option<char> {
match key {
Key::KEY_1 => Some('1'),
Key::KEY_2 => Some('2'),
Key::KEY_3 => Some('3'),
Key::KEY_4 => Some('4'),
Key::KEY_5 => Some('5'),
Key::KEY_6 => Some('6'),
Key::KEY_7 => Some('7'),
Key::KEY_8 => Some('8'),
Key::KEY_9 => Some('9'),
Key::KEY_0 => Some('0'),
_ => None,
}
}

fn play_sound(
inputs_to_filenames: &HashMap<String, String>,
input: &str,
dir: &Path,
sink: &Sink,
) -> Result<()> {
match inputs_to_filenames.get(input.trim()) {
Some(filename) => {
let path = dir.join(filename);
if !&path.exists() {
eprintln!("Sound file {} does not exist.", path.display());
return Ok(());
}
let source = load_source(&path)?;
sink.append(source);
}
_ => (),
}
Ok(())
}

fn load_source(path: &Path) -> Result<Decoder<BufReader<File>>> {
let file = BufReader::new(File::open(path)?);
Ok(Decoder::new(file)?)
}

fn main() -> Result<()> {
let args = parse_args();

let config_filename = args.value_of("config").map(Path::new).unwrap();
let config = load_config(config_filename)?;

let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let sink = Sink::try_new(&stream_handle).unwrap();

sink.sleep_until_end();

let input_device_path = args.value_of("input_device").unwrap();
let mut input_device = Device::open(input_device_path)?;
println!(
"Opened input device \"{}\".",
input_device.name().unwrap_or("unnamed device")
);

match input_device.grab() {
Ok(_) => println!("Successfully obtained exclusive access to input device."),
Err(error) => {
eprintln!("Could not get exclusive access to input device: {}", error);
exit(1);
}
}

let mut read_chars = String::new();
loop {
for event in input_device.fetch_events()? {
// Only handle pressed key events.
if event.event_type() != EventType::KEY || event.value() == 1 {
continue;
}

match event.kind() {
InputEventKind::Key(Key::KEY_ENTER) => {
let input = read_chars.as_str();
play_sound(
&config.inputs_to_filenames,
input,
config.sounds_path.as_path(),
&sink,
)?;
read_chars.clear();
}
InputEventKind::Key(key) => {
match get_char(key) {
Some(ch) => read_chars.push(ch),
None => (),
};
}
_ => (),
}
}
}
}

0 comments on commit c5b347e

Please sign in to comment.