Skip to content

Commit

Permalink
feat(playback): add random variation on each keypress (#24)
Browse files Browse the repository at this point in the history
* feat(sound): Add random variation on each keypress

* refactor(sound): remove pitch, change default and improve cli

* docs(readme): add example for variation

* docs(readme): use snake case for key config

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Orhun Parmaksız <[email protected]>
  • Loading branch information
3 people committed Oct 18, 2023
1 parent 7d347ad commit c53334c
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 25 deletions.
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Now you can recreate this moment without the actual need for a physical typewrit
- [Usage](#usage)
- [Configuration](#configuration)
- [Adding custom presets](#adding-custom-presets)
- [Sound Variation](#sound-variation)
- [Similar Projects](#similar-projects)
- [Acknowledgements](#acknowledgements)
- [Donations](#donations)
Expand Down Expand Up @@ -93,6 +94,12 @@ daktilo --device pipewire

Also, you can use `--list-devices` to list the available output devices.

To variate the sounds and have a more realistic typewriter experience:

```sh
daktilo --variate-tempo 0.9,0.4 --variate-volume 0.1,0.5
```

<details>
<summary>Spoiler warning</summary>

Expand Down Expand Up @@ -192,15 +199,17 @@ daktilo [OPTIONS]
**Options**:

```sh
-v, --verbose Enables verbose logging [env: VERBOSE=]
-p, --preset [<PRESET>...] Sets the name of the sound preset to use [env: PRESET=]
-l, --list-presets Lists the available presets
--list-devices Lists the available output devices
-d, --device <DEVICE> Sets the device for playback [env: DAKTILO_DEVICE=]
-c, --config <PATH> Sets the configuration file [env: DAKTILO_CONFIG=]
-i, --init Writes the default configuration file
-h, --help Print help (see more with '--help')
-V, --version Print version
-v, --verbose Enables verbose logging [env: VERBOSE=]
-p, --preset [<PRESET>...] Sets the name of the sound preset to use [env: PRESET=]
-l, --list-presets Lists the available presets
--list-devices Lists the available output devices
-d, --device <DEVICE> Sets the device for playback [env: DAKTILO_DEVICE=]
-c, --config <PATH> Sets the configuration file [env: DAKTILO_CONFIG=]
-i, --init Writes the default configuration file
--variate-volume <PERCENT_UP[,PERCENT_DOWN]> Variate volume +/- in percent [env: DAKTILO_VOLUME=]
--variate-tempo <PERCENT_UP[,PERCENT_DOWN]> Variate tempo +/- in percent [env: DAKTILO_TEMPO=]
-h, --help Print help (see more with '--help')
-V, --version Print version
```

## Configuration
Expand Down Expand Up @@ -246,13 +255,15 @@ key_config = []
name = "another_custom"
key_config = []
disabled_keys = []
variation = { volume: [0.1, 0.1], tempo: [0.05, 0.05] }
```

As shown above, `sound_preset` consists of 2 entries:

- `name`: The name of the preset. It will be used in conjunction with `--preset` flag. e.g. `--preset custom`
- `key_config`: An array of key press/release events for assigning audio files to the specified keys. It can also be used to control the volume etc.
- `disabled_keys`: An array of keys that will not be used for playback.
- `variation`: Variate the sound on each event for `key_config`s that do not specify variations[\*](#sound-variation)

<details>
<summary>Click for the <a href="https://docs.rs/rdev/latest/rdev/enum.Key.html">list of available keys</a>.</summary>
Expand All @@ -274,6 +285,7 @@ key_config = [
- `files`: An array of files.
- `path`: The absolute path of the file. If the file is embedded in the binary (i.e. if it is inside `sounds/` directory) then it is the name of the file without full path.
- `volume`: The volume of the sound. The value 1.0 is the "normal" volume (unfiltered input). Any value other than 1.0 will multiply each sample by this value.
- `variation`: Variate the sound on each `event`[\*](#sound-variation)

If you have defined multiple files for a key event, you can also specify a strategy for how to play them:

Expand Down Expand Up @@ -319,13 +331,25 @@ key_config = [
{ path = "cat.mp3" },
{ path = "dog.mp3" },
{ path = "bird.mp3" },
], strategy = "random" },
], strategy = "random", variation = { volume: [0.1, 0.1], tempo: [0.05, 0.05] } },
]

# Disabled keys that won't trigger any sound events
disabled_keys = ["CapsLock", "NumLock"]
```

### Sound Variation

To make the keyboard sounds more varied it is possible to variate both volume and playback speed (the later also varies the pitch).

Values are in percent, where the first value determines the maximum increase and the second the maximum decrease. The actual value is determined randomly on each keypress.

- If command line arguments or environment variables are set configurations made in the presets are overridden.
- Values need to be separated by a ",".
- If only one value is supplied it is used for both increase and decrease
- If a `key_config` is set the preset values are overridden.
- The configuration on a preset applies to all `key_config`'s that do not have any values set.

## Similar Projects

- [`bucklespring`](https://github.com/zevv/bucklespring): Nostalgia bucklespring keyboard sound
Expand Down
4 changes: 2 additions & 2 deletions config/daktilo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ key_config = [
{ event = "press", keys = "Return", files = [
{ path = "ding.mp3", volume = 1.0 },
] },
{ event = "press", keys = ".*", files = [
{ event = "press", keys = ".*", variation = { volume = [0.1, 0.1], tempo = [0.05, 0.05] }, files = [
{ path = "keydown.mp3", volume = 1.0 },
] },
{ event = "release", keys = ".*", files = [
{ event = "release", keys = ".*", variation = { volume = [0.1, 0.1], tempo = [0.05, 0.05] }, files = [
{ path = "keyup.mp3", volume = 1.0 },
] },
]
Expand Down
58 changes: 49 additions & 9 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
config::{AudioFile, KeyConfig, KeyEvent, PlaybackStrategy, SoundPreset},
config::{AudioFile, KeyConfig, KeyEvent, PlaybackStrategy, SoundPreset, SoundVariation},
embed::EmbeddedSound,
error::{Error, Result},
};
Expand All @@ -21,11 +21,17 @@ pub struct App {
key_released: bool,
/// Index of the file to play.
file_index: usize,
/// Sound variations.
variation: Option<SoundVariation>,
}

impl App {
/// Initializes a new instance.
pub fn init(preset: SoundPreset, device: Option<String>) -> Result<Self> {
pub fn init(
preset: SoundPreset,
variation: Option<SoundVariation>,
device: Option<String>,
) -> Result<Self> {
let device = match device {
Some(ref device) => rodio::cpal::default_host()
.output_devices()?
Expand All @@ -50,6 +56,7 @@ impl App {
key_release_sink,
key_released: true,
file_index: 0,
variation,
})
}

Expand Down Expand Up @@ -97,7 +104,7 @@ impl App {
if self.key_released {
if let Some(key_config) = key_config {
let file = self.pick_sound_file(key_config)?;
self.play_sound(&file, &self.key_press_sink)?;
self.play_sound(&file, self.get_variation(key_config), &self.key_press_sink)?;
}
}
self.key_released = false;
Expand All @@ -108,7 +115,11 @@ impl App {
fn handle_key_release(&mut self, key_config: &Option<KeyConfig>) -> Result<()> {
if let Some(key_config) = key_config {
let file = self.pick_sound_file(key_config)?;
self.play_sound(&file, &self.key_release_sink)?;
self.play_sound(
&file,
self.get_variation(key_config),
&self.key_release_sink,
)?;
}
self.key_released = true;
Ok(())
Expand Down Expand Up @@ -137,21 +148,50 @@ impl App {
}

/// Play the sound from embedded/file for the given sink.
fn play_sound(&self, file: &AudioFile, sink: &Sink) -> Result<()> {
fn play_sound(
&self,
file: &AudioFile,
variation: Option<SoundVariation>,
sink: &Sink,
) -> Result<()> {
tracing::debug!("Playing: {:?}", file);

let volume = file.volume.unwrap_or(1.0)
* self.generate_variation_factor(variation.as_ref().and_then(|v| v.volume));
let tempo = self.generate_variation_factor(variation.as_ref().and_then(|v| v.tempo));
tracing::debug!("Volume: {}, Tempo: {}", volume, tempo);

sink.stop();
sink.set_volume(volume);
sink.set_speed(tempo);

if let Some(embed_data) = EmbeddedSound::get_sound(&file.path) {
let sound = BufReader::new(Box::new(embed_data));
sink.stop();
sink.set_volume(file.volume.unwrap_or(1.0));
sink.append(Decoder::new(sound)?);
} else {
let sound = BufReader::new(Box::new(File::open(&file.path)?));
sink.stop();
sink.set_volume(file.volume.unwrap_or(1.0));
sink.append(Decoder::new(sound)?);
};
Ok(())
}

/// Get variation for the given key.
fn get_variation(&self, key: &KeyConfig) -> Option<SoundVariation> {
self.variation
.clone()
.or(key.variation.clone())
.or(self.preset.variation.clone())
}

/// Generate variation factor
fn generate_variation_factor(&self, variation: Option<(f32, f32)>) -> f32 {
let Some((plus, minus)) = variation else {
return 1.0;
};

let variation = fastrand::f32() * (plus + minus) - minus;
1.0 + variation
}
}

#[cfg(feature = "audio-tests")]
Expand Down
47 changes: 47 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::path::PathBuf;

use clap::Parser;

use crate::config::SoundVariation;

/// Typewriter ASCII banner.
pub const BANNER: &str = r#"
.-------.
Expand Down Expand Up @@ -57,6 +59,51 @@ pub struct Args {
/// Disables the easter eggs.
#[arg(long, hide = true)]
pub no_surprises: bool,
/// Variate pitch/volume/tempo.
#[command(flatten)]
pub sound_variation_args: Option<SoundVariationArgs>,
}

/// Variate pitch/volume/tempo.
#[derive(clap::Args, Default, Debug)]
pub struct SoundVariationArgs {
/// Variate volume +/- in percent.
#[arg(
long,
env = "DAKTILO_VOLUME",
value_name = "PERCENT_UP[,PERCENT_DOWN]",
value_delimiter = ',',
num_args(1..2)
)]
pub variate_volume: Option<Vec<f32>>,
/// Variate tempo +/- in percent.
#[arg(
long,
env = "DAKTILO_TEMPO",
value_name = "PERCENT_UP[,PERCENT_DOWN]",
value_delimiter = ',',
num_args(1..2)
)]
pub variate_tempo: Option<Vec<f32>>,
}

impl From<SoundVariationArgs> for SoundVariation {
fn from(args: SoundVariationArgs) -> Self {
Self {
volume: args.variate_volume.map(|v| {
(
v.first().cloned().unwrap_or(1.0),
v.get(1).or(v.first()).cloned().unwrap_or(1.0),
)
}),
tempo: args.variate_tempo.map(|t| {
(
t.first().cloned().unwrap_or(1.0),
t.get(1).or(t.first()).cloned().unwrap_or(1.0),
)
}),
}
}
}

#[cfg(test)]
Expand Down
20 changes: 20 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ impl Config {
},
],
strategy: Some(PlaybackStrategy::Random),
variation: None,
},
KeyConfig {
event: KeyEvent::KeyPress,
Expand All @@ -84,9 +85,14 @@ impl Config {
volume: None,
}],
strategy: None,
variation: Some(SoundVariation {
volume: Some((0.1, 0.1)),
tempo: Some((0.075, 0.075)),
}),
},
],
disabled_keys: None,
variation: None,
});
}
self.sound_presets
Expand All @@ -106,6 +112,8 @@ pub struct SoundPreset {
pub key_config: Vec<KeyConfig>,
/// List of disabled keys.
pub disabled_keys: Option<Vec<Key>>,
/// Configure sound variations.
pub variation: Option<SoundVariation>,
}

impl fmt::Display for SoundPreset {
Expand Down Expand Up @@ -156,6 +164,8 @@ pub struct KeyConfig {
pub files: Vec<AudioFile>,
/// Playback strategy.
pub strategy: Option<PlaybackStrategy>,
/// Sound variations. Overrides the preset sound variations.
pub variation: Option<SoundVariation>,
}

/// Key event type.
Expand Down Expand Up @@ -189,6 +199,16 @@ pub enum PlaybackStrategy {
Sequential,
}

/// Sound variation configuration.
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct SoundVariation {
/// Volume +/- in percent.
pub volume: Option<(f32, f32)>,
/// Tempo +/- in percent.
pub tempo: Option<(f32, f32)>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
10 changes: 7 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ pub mod app;
pub mod config;

use app::App;
use config::SoundPreset;
use config::{SoundPreset, SoundVariation};
use error::Result;
use rdev::listen;
use std::thread;

/// Starts the typewriter.
pub async fn run(sound_presets: Vec<SoundPreset>, device: Option<String>) -> Result<()> {
pub async fn run(
sound_presets: Vec<SoundPreset>,
variation: Option<SoundVariation>,
device: Option<String>,
) -> Result<()> {
// Create a listener for the keyboard events.
let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
thread::spawn(move || {
Expand All @@ -43,7 +47,7 @@ pub async fn run(sound_presets: Vec<SoundPreset>, device: Option<String>) -> Res
tracing::debug!("{:#?}", sound_presets);
let mut apps = sound_presets
.into_iter()
.map(|sound_preset| App::init(sound_preset, device.clone()))
.map(|sound_preset| App::init(sound_preset, variation.clone(), device.clone()))
.collect::<Result<Vec<_>>>()?;

// Handle events.
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ async fn main() -> Result<()> {
.map(|name| config.select_preset(name))
.collect::<Result<Vec<_>>>()?;

match daktilo::run(presets, args.device).await {
match daktilo::run(
presets,
args.sound_variation_args.map(|v| v.into()),
args.device,
)
.await
{
Ok(_) => process::exit(0),
Err(e) => {
tracing::error!("error occurred: {e}");
Expand Down

0 comments on commit c53334c

Please sign in to comment.