sfx.rs 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. //! UI sound effects played through rodio (cpal underneath).
  19. //!
  20. //! The audio device is opened lazily on the first sound played. If the
  21. //! device cannot be opened, sounds are silently disabled for the session.
  22. //! On Android the JavaVM/Activity context must be registered with
  23. //! `ndk_context` before cpal initializes its AAudio backend.
  24. use rodio::Source;
  25. use std::{io::Cursor, sync::LazyLock};
  26. macro_rules! e { ($($arg:tt)*) => { error!(target: "app::sfx", $($arg)*); } }
  27. static CLICK_OGA: &[u8] = include_bytes!("../data/sfx/click.oga");
  28. static COMMUP_OGA: &[u8] = include_bytes!("../data/sfx/commup.oga");
  29. struct Sfx {
  30. /// Keeps the output stream alive for the process lifetime
  31. _output: rodio::MixerDeviceSink,
  32. /// Shared mixer the sounds get appended to
  33. mixer: rodio::mixer::Mixer,
  34. /// Decoded click sound, cloned on every play
  35. click: rodio::buffer::SamplesBuffer,
  36. /// Decoded startup sound
  37. commup: rodio::buffer::SamplesBuffer,
  38. }
  39. static SFX: LazyLock<Option<Sfx>> = LazyLock::new(|| match init() {
  40. Ok(sfx) => Some(sfx),
  41. Err(err) => {
  42. e!("Audio init failed, disabling sounds: {err}");
  43. None
  44. }
  45. });
  46. fn init() -> Result<Sfx, Box<dyn std::error::Error>> {
  47. #[cfg(target_os = "android")]
  48. init_android_context();
  49. let output = rodio::DeviceSinkBuilder::open_default_sink()?;
  50. let mixer = output.mixer().clone();
  51. let click = rodio::Decoder::try_from(Cursor::new(CLICK_OGA))?.record();
  52. let commup = rodio::Decoder::try_from(Cursor::new(COMMUP_OGA))?.record();
  53. Ok(Sfx { _output: output, mixer, click, commup })
  54. }
  55. /// cpal's AAudio backend fetches the JavaVM and Activity from the
  56. /// `ndk-context` crate, which miniquad does not initialize itself.
  57. #[cfg(target_os = "android")]
  58. fn init_android_context() {
  59. use miniquad::native::android;
  60. unsafe {
  61. let env = crate::android::get_jni_env();
  62. let mut vm: *mut android::ndk_sys::JavaVM = std::ptr::null_mut();
  63. let get_java_vm = (**env).GetJavaVM.unwrap();
  64. assert_eq!(get_java_vm(env, &mut vm), 0);
  65. assert!(!vm.is_null());
  66. assert!(!android::ACTIVITY.is_null());
  67. ndk_context::initialize_android_context(vm as *mut _, android::ACTIVITY as *mut _);
  68. }
  69. }
  70. pub fn play_click() {
  71. if let Some(sfx) = SFX.as_ref() {
  72. sfx.mixer.add(sfx.click.clone());
  73. }
  74. }
  75. pub fn play_commup() {
  76. if let Some(sfx) = SFX.as_ref() {
  77. sfx.mixer.add(sfx.commup.clone());
  78. }
  79. }