Skip to content

Commit de4190a

Browse files
committed
feat(AudioPlayer): allow weighted random selection
This feature allows bloop and confirm sounds to be randomly selected based on their weight. By default an audio file has a weight of 100. This can be modified by prepending `.[w=<weight>]` before the `.mp3` extension. Weight can be any integer or float greater or equal to zero.
1 parent 20a8e90 commit de4190a

File tree

3 files changed

+93
-37
lines changed

3 files changed

+93
-37
lines changed

Cargo.lock

Lines changed: 22 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ linux-embedded-hal = "0.3.2"
3535
embedded-hal = "0.2.7"
3636
rodio = { version = "0.16.0", features = ["symphonia-mp3"] }
3737
aw2013 = "1.0.0"
38+
rand_distr = "0.4.3"
39+
regex = "1.7.1"
40+
lazy_static = "1.4.0"
3841

3942
[profile.release]
4043
strip = "debuginfo"

src/subsystems/audio_player.rs

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,20 @@ use std::thread::sleep;
66
use std::time::Duration;
77

88
use crate::subsystems::config_manager::ConfigCommand;
9-
use anyhow::{anyhow, Error, Result};
9+
10+
use anyhow::{bail, Error, Result};
1011
use glob::glob;
12+
use lazy_static::lazy_static;
1113
use log::info;
12-
use rand::seq::SliceRandom;
14+
use rand_distr::Distribution;
15+
use rand_distr::WeightedAliasIndex;
16+
use regex::Regex;
1317
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
1418
use tokio::sync::mpsc;
1519
use tokio::sync::mpsc::error::TryRecvError;
1620
use tokio::sync::oneshot;
1721
use tokio_graceful_shutdown::{IntoSubsystem, SubsystemHandle};
1822

19-
pub struct AudioPlayer {
20-
share_path: PathBuf,
21-
cache_path: PathBuf,
22-
bloop_paths: Vec<PathBuf>,
23-
confirm_paths: Vec<PathBuf>,
24-
rx: mpsc::Receiver<PlayerCommand>,
25-
config: mpsc::Sender<ConfigCommand>,
26-
}
27-
2823
pub type Done = oneshot::Sender<()>;
2924

3025
#[derive(Debug)]
@@ -147,6 +142,60 @@ impl InternalPlayer {
147142
}
148143
}
149144

145+
struct AudioCollection {
146+
paths: Vec<PathBuf>,
147+
dist: WeightedAliasIndex<f64>,
148+
}
149+
150+
impl AudioCollection {
151+
pub fn from_dir(path: &Path) -> Result<AudioCollection> {
152+
let mut paths: Vec<PathBuf> = Vec::new();
153+
154+
for entry in glob(format!("{}/*.mp3", path.to_str().unwrap()).as_str()).unwrap() {
155+
paths.push(entry.unwrap().as_path().try_into()?);
156+
}
157+
158+
if paths.is_empty() {
159+
bail!("Path '{:?}' contains no mp3 files", path);
160+
}
161+
162+
let mut weights: Vec<f64> = Vec::new();
163+
164+
lazy_static! {
165+
static ref RE: Regex = Regex::new(r"\.\[w=(\d+(?:\.\d*)?)\]\.mp3$").unwrap();
166+
}
167+
168+
for path in &paths {
169+
let filename = path.file_name().unwrap();
170+
let cap = RE.captures(filename.to_str().unwrap());
171+
172+
weights.push(if let Some(cap) = cap {
173+
cap[1].parse::<f64>()?
174+
} else {
175+
100.
176+
});
177+
}
178+
179+
Ok(AudioCollection {
180+
paths,
181+
dist: WeightedAliasIndex::new(weights)?,
182+
})
183+
}
184+
185+
pub fn choose_random(&self) -> &PathBuf {
186+
&self.paths[self.dist.sample(&mut rand::thread_rng())]
187+
}
188+
}
189+
190+
pub struct AudioPlayer {
191+
share_path: PathBuf,
192+
cache_path: PathBuf,
193+
bloop_collection: AudioCollection,
194+
confirm_collection: AudioCollection,
195+
rx: mpsc::Receiver<PlayerCommand>,
196+
config: mpsc::Sender<ConfigCommand>,
197+
}
198+
150199
impl AudioPlayer {
151200
pub fn new(
152201
share_path: PathBuf,
@@ -157,8 +206,10 @@ impl AudioPlayer {
157206
Self {
158207
share_path: share_path.clone(),
159208
cache_path,
160-
bloop_paths: Self::collect_paths(&share_path, "bloop").expect(""),
161-
confirm_paths: Self::collect_paths(&share_path, "confirm").expect(""),
209+
bloop_collection: AudioCollection::from_dir(&share_path.join(Path::new("bloop")))
210+
.unwrap(),
211+
confirm_collection: AudioCollection::from_dir(&share_path.join(Path::new("confirm")))
212+
.unwrap(),
162213
rx,
163214
config,
164215
}
@@ -168,6 +219,8 @@ impl AudioPlayer {
168219
let (internal_tx, internal_rx) = mpsc::channel(8);
169220
let share_path = self.share_path.to_owned();
170221

222+
info!("selected: {:?}", self.bloop_collection.choose_random());
223+
171224
thread::spawn(move || {
172225
let internal_player =
173226
InternalPlayer::new(internal_rx, share_path.join(Path::new("volume-change.mp3")))
@@ -194,11 +247,7 @@ impl AudioPlayer {
194247

195248
match play_command {
196249
PlayBloop { done } => {
197-
let path = self
198-
.bloop_paths
199-
.choose(&mut rand::thread_rng())
200-
.ok_or_else(|| anyhow!("No boop files available"))?
201-
.clone();
250+
let path = self.bloop_collection.choose_random().clone();
202251
internal_tx
203252
.send(InternalCommand::PlayFile {
204253
path: self.share_path.join(path),
@@ -207,11 +256,7 @@ impl AudioPlayer {
207256
.await?;
208257
}
209258
PlayConfirm { done } => {
210-
let path = self
211-
.confirm_paths
212-
.choose(&mut rand::thread_rng())
213-
.ok_or_else(|| anyhow!("No confirm files available"))?
214-
.clone();
259+
let path = self.confirm_collection.choose_random().clone();
215260
internal_tx
216261
.send(InternalCommand::PlayFile {
217262
path: self.share_path.join(path),
@@ -280,18 +325,6 @@ impl AudioPlayer {
280325

281326
Ok(())
282327
}
283-
284-
fn collect_paths(share_path: &Path, dir_name: &str) -> Result<Vec<PathBuf>> {
285-
let mut paths: Vec<PathBuf> = Vec::new();
286-
287-
for entry in
288-
glob(format!("{}/{}/*.mp3", share_path.to_str().unwrap(), dir_name).as_str()).unwrap()
289-
{
290-
paths.push(entry.unwrap().as_path().try_into()?);
291-
}
292-
293-
Ok(paths)
294-
}
295328
}
296329

297330
#[async_trait::async_trait]

0 commit comments

Comments
 (0)