Skip to content

Commit

Permalink
test(sound): check sound files in rust tests
Browse files Browse the repository at this point in the history
instead of invoking soxi
check that all files are present in the assets
check bits per sample (16 bits)
SOUNDS_DIR can be overwritten at runtime with an env variable
tests are run with all other rust tests
  • Loading branch information
fouge committed Oct 28, 2024
1 parent a53f4cc commit ae6aad3
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 105 deletions.
23 changes: 0 additions & 23 deletions .github/workflows/check-sound-files.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/rust-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ jobs:
- name: Cargo Test
run: |
nix develop -c \
cargo test --all --all-features --all-targets $EXCLUDES
SOUNDS_DIR=$(pwd)/ui/sounds/assets >>${GITHUB_ENV} cargo test --all --all-features --all-targets $EXCLUDES
build:
name: Build
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ path = "examples/ui-replay.rs"
# dependencies for the dbus-client example
[dev-dependencies]
chrono = "0.4.35"
hound = "3.5.1"

[package.metadata.deb]
maintainer-scripts = "debian/"
Expand Down
17 changes: 5 additions & 12 deletions ui/sound/examples/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ impl AsRef<[u8]> for Sound {
async fn main() -> Result<()> {
let queue = Queue::spawn("default")?;

let connected = Sound(Arc::new(fs::read("sound/examples/voice_connected.wav")?));
let overheating =
Sound(Arc::new(fs::read("sound/examples/voice_overheating.wav")?));
let connected = Sound(Arc::new(fs::read("sound/assets/voice_connected.wav")?));
let timeout = Sound(Arc::new(fs::read("sound/assets/voice_timeout.wav")?));

// Said firstly because the queue starts playing immediately.
queue
Expand All @@ -40,19 +39,13 @@ async fn main() -> Result<()> {
// Said secondly because it has a higher priority than all pending sounds in
// the queue.
queue
.sound(
Some(Cursor::new(overheating.clone())),
"overheating".to_string(),
)
.sound(Some(Cursor::new(timeout.clone())), "timeout".to_string())
.priority(1)
.push()?;
// Not said because it doesn't meet the `max_delay`.
assert!(
!queue
.sound(
Some(Cursor::new(overheating.clone())),
"overheating".to_string()
)
.sound(Some(Cursor::new(timeout.clone())), "timeout".to_string())
.priority(1)
.max_delay(Duration::from_secs(1))
.push()?
Expand All @@ -72,7 +65,7 @@ async fn main() -> Result<()> {
);

// In result the queue should be played in the following order: connected,
// overheating, connected, connected.
// timeout, connected, connected.

Ok(())
}
3 changes: 0 additions & 3 deletions ui/sound/examples/voice_connected.wav

This file was deleted.

3 changes: 0 additions & 3 deletions ui/sound/examples/voice_overheating.wav

This file was deleted.

2 changes: 1 addition & 1 deletion ui/sound/examples/wav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ fn main() -> Result<()> {
let mut device = Device::open("default")?;
let mut hw_params = HwParams::new()?;

let mut wav = File::open("sound/examples/voice_connected.wav")?;
let mut wav = File::open("sound/assets/voice_connected.wav")?;
device.play_wav(&mut wav, &mut hw_params, 1.0)?;
device.drain()?;

Expand Down
9 changes: 0 additions & 9 deletions ui/sound/utils/README.md

This file was deleted.

52 changes: 0 additions & 52 deletions ui/sound/utils/check_sounds.sh

This file was deleted.

126 changes: 125 additions & 1 deletion ui/src/sound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,10 @@ async fn load_sound_file(
language: Option<&str>,
ignore_missing: bool,
) -> Result<SoundFile> {
let sounds_dir = Path::new(SOUNDS_DIR);
let path = std::env::var("SOUNDS_DIR")
.unwrap_or(SOUNDS_DIR.to_string())
.clone();
let sounds_dir = Path::new(&path);
if let Some(language) = language {
let file = sounds_dir.join(format!("{sound}__{language}.wav"));
if file.exists() {
Expand All @@ -320,6 +323,22 @@ async fn load_sound_file(
}
}
};

// we have had errors with reading files encoded over 24 bits, so
// this test ensure that wav files are sampled on 16 bits, for full Jetson compatibility.
// remove this test if different sampling are supported.
#[cfg(test)]
{
let reader = hound::WavReader::open(&file)
.map_err(|e| eyre::eyre!("hound: {:?}: {:#?}", &file, e))?;
assert_eq!(
reader.spec().bits_per_sample,
16,
"Only 16-bit sounds are supported: {:?}",
&file
);
}

Ok(SoundFile(Arc::new(data)))
}

Expand All @@ -328,3 +347,108 @@ impl fmt::Debug for Jetson {
f.debug_struct("Sound").finish()
}
}

#[cfg(test)]
mod tests {
use super::{Melody, Player, SoundFile, Type, Voice};
use dashmap::DashMap;
use orb_sound::SoundBuilder;
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;

struct MockJetson {
sound_files: Arc<DashMap<Type, SoundFile>>,
}

impl Debug for MockJetson {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MockJetson").finish()
}
}

impl Player for MockJetson {
fn load_sound_files(
&self,
language: Option<&str>,
ignore_missing_sounds: bool,
) -> Pin<Box<dyn Future<Output = eyre::Result<()>> + Send + '_>> {
let sound_files = Arc::clone(&self.sound_files);
let language = language.map(ToOwned::to_owned);
Box::pin(async move {
Voice::load_sound_files(
&sound_files,
language.as_deref(),
ignore_missing_sounds,
)
.await?;
Melody::load_sound_files(
&sound_files,
language.as_deref(),
ignore_missing_sounds,
)
.await?;
let count = sound_files.len();
tracing::debug!("Sound files for language {language:?} loaded successfully ({count:?} files)");
Ok(())
})
}

fn build(&mut self, _sound_type: Type) -> eyre::Result<SoundBuilder> {
unimplemented!()
}

fn clone(&self) -> Box<dyn Player> {
Box::new(MockJetson {
sound_files: self.sound_files.clone(),
})
}

fn volume(&self) -> u64 {
unimplemented!()
}

fn set_master_volume(&mut self, _level: u64) {
unimplemented!()
}

fn queue(&mut self, _sound_type: Type, _delay: Duration) -> eyre::Result<()> {
unimplemented!()
}

fn try_queue(&mut self, _sound_type: Type) -> eyre::Result<bool> {
unimplemented!()
}
}

/// This test allows us to check that all files that can be pulled by the UI
/// are present in the repository and are all encoded over 16 bits
#[tokio::test]
async fn test_load_sound_file() {
let sound = MockJetson {
sound_files: Arc::new(DashMap::new()),
};

let language = None;
let res = sound.load_sound_files(language, false).await;
if let Err(e) = &res {
println!("{:?}", e);
}
assert!(res.is_ok(), "Default (None) failed");

let language = Some("EN-en");
let res = sound.load_sound_files(language, false).await;
assert!(res.is_ok(), "EN-en failed");

let language = Some("ES-es");
let res = sound.load_sound_files(language, false).await;
assert!(res.is_ok(), "ES-en failed");

// unsupported / missing voice files
let language = Some("FR-fr");
let res = sound.load_sound_files(language, false).await;
assert!(res.is_ok(), "ES-en failed");
}
}

0 comments on commit ae6aad3

Please sign in to comment.