diff --git a/.github/workflows/check-sound-files.yaml b/.github/workflows/check-sound-files.yaml deleted file mode 100644 index 3c74ab2b..00000000 --- a/.github/workflows/check-sound-files.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: Check Sound Files - -on: - push: - paths: - - 'ui/sound/assets/**' - - '.github/workflows/check-sound-files.yaml' - -jobs: - sound-files: - name: Check Sound Files - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 - with: - lfs: true - - run: sudo apt-get install -y sox - - name: Check sound files - run: | - bash ./ui/sound/utils/check_sounds.sh ui/sound/assets/ - # ensure error code is 0 - exit $? diff --git a/.github/workflows/rust-ci.yaml b/.github/workflows/rust-ci.yaml index 3ca05c0d..e484fca6 100644 --- a/.github/workflows/rust-ci.yaml +++ b/.github/workflows/rust-ci.yaml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 048b10b3..75f0da9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4665,6 +4665,7 @@ dependencies = [ "derive_more", "eyre", "futures", + "hound", "orb-build-info 0.0.0", "orb-messages 0.0.0 (git+https://github.com/worldcoin/orb-messages?rev=787ab78581b705af0946bcfe3a0453b64af2193f)", "orb-rgb", diff --git a/ui/Cargo.toml b/ui/Cargo.toml index f3f1954c..5e06a948 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -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/" diff --git a/ui/sound/examples/queue.rs b/ui/sound/examples/queue.rs index 0bb3219e..9d148f84 100644 --- a/ui/sound/examples/queue.rs +++ b/ui/sound/examples/queue.rs @@ -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 @@ -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()? @@ -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(()) } diff --git a/ui/sound/examples/voice_connected.wav b/ui/sound/examples/voice_connected.wav deleted file mode 100644 index e37516ab..00000000 --- a/ui/sound/examples/voice_connected.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:174676b7611a291ec5cb86c7ef4f29740333ca3e26c2913bddcad32f92b88dd4 -size 53036 diff --git a/ui/sound/examples/voice_overheating.wav b/ui/sound/examples/voice_overheating.wav deleted file mode 100644 index 8f5c4d8b..00000000 --- a/ui/sound/examples/voice_overheating.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6b2758a49eea5ef0ca9e75a934133675d1b7034de79c3cce507812b34e7bbdf -size 57408 diff --git a/ui/sound/examples/wav.rs b/ui/sound/examples/wav.rs index d5fbd688..a405fcdb 100644 --- a/ui/sound/examples/wav.rs +++ b/ui/sound/examples/wav.rs @@ -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()?; diff --git a/ui/sound/utils/README.md b/ui/sound/utils/README.md deleted file mode 100644 index f09dd635..00000000 --- a/ui/sound/utils/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Sound utils - -## `check_sounds.sh` - -Ensure that all sounds are `.wav` files with 16-bit/sample. - -```shell -$ bash ./check_sounds.sh ../assets/ -``` diff --git a/ui/sound/utils/check_sounds.sh b/ui/sound/utils/check_sounds.sh deleted file mode 100644 index c0f30dc0..00000000 --- a/ui/sound/utils/check_sounds.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit # abort on nonzero exitstatus -set -o nounset # abort on unbound variable -set -o pipefail # don't hide errors within pipes - -# Checks that the sounds directory only includes *.wav files -# Checks that the sounds are sampled on 16 bits -# Guards against bad archive preparation. - -validate_sounds::validate() { - local -r sounds_dir=${1} - - pushd "${sounds_dir}" >/dev/null - - local check_bit_sampling=true - if ! [[ -x "$(command -v soxi)" ]]; then - echo "Install sox (soxi) to check bits per sample" >>/dev/stderr - check_bit_sampling=false - fi - - for file in *; do - if ! [[ "${file}" =~ \.wav$ ]] ; then - echo "Invalid sounds directory: '${file}' is not a wav file" - exit 1 - fi - - if [[ "${check_bit_sampling}" = true ]]; then - local -i bits_per_sample - bits_per_sample="$(soxi -b "${file}")" - - if [[ "${bits_per_sample}" != 16 ]]; then - echo "${file} must be converted to 16 bits per sample" - exit 1 - fi - fi - done - - popd >/dev/null - - echo "Sounds directory successfully validated." -} - -main() { - trap trap_err ERR - - validate_sounds::validate "${1}" -} - -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi diff --git a/ui/src/sound/mod.rs b/ui/src/sound/mod.rs index 5ae4273c..0d9baab2 100644 --- a/ui/src/sound/mod.rs +++ b/ui/src/sound/mod.rs @@ -295,7 +295,10 @@ async fn load_sound_file( language: Option<&str>, ignore_missing: bool, ) -> Result { - 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() { @@ -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))) } @@ -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>, + } + + 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> + 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 { + unimplemented!() + } + + fn clone(&self) -> Box { + 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 { + 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"); + } +}