sounds.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. """Related to sounds"""
  2. import asyncio
  3. import os
  4. import random
  5. import wx # type: ignore
  6. from collections import defaultdict
  7. from concurrent.futures import ThreadPoolExecutor
  8. from openal import AL_PLAYING, PYOGG_AVAIL, Buffer, OpusFile, Source # type: ignore
  9. from threading import Lock
  10. from typing import Dict, List
  11. from wxasync import StartCoroutine # type: ignore
  12. import config
  13. class SoundManager:
  14. """Loads and plays sounds"""
  15. def __init__(self, client) -> None:
  16. self.playerid = None
  17. self.client = client
  18. self.lock = Lock()
  19. self.nb_max_sounds = 0
  20. # Dict[category:List[sound_data]]
  21. self.loaded_sounds: Dict[str, List[Buffer]] = defaultdict(list)
  22. self.volume: int = config.config["Sounds"].getint("Volume", 50) # type: ignore
  23. def max_sounds(self) -> int:
  24. """Returns the number of sounds that will be loaded."""
  25. max = 0
  26. for path in os.listdir("sounds"):
  27. for file in os.listdir(os.path.join("sounds", path)):
  28. if file.startswith(".git") or file == "desktop.ini":
  29. continue
  30. max = max + 1
  31. return max
  32. def load(self, category: str, filepath: str) -> None:
  33. # Can't load files - TODO show error & quit
  34. if not PYOGG_AVAIL:
  35. return
  36. with self.lock:
  37. self.loaded_sounds[category].append(Buffer(OpusFile(filepath)))
  38. wx.CallAfter(
  39. self.client.gui.SetStatusText,
  40. f"Loading sounds... ({len(self.loaded_sounds)}/{self.nb_max_sounds})",
  41. )
  42. async def reload(self) -> None:
  43. """Reloads the list of available sounds.
  44. The async logic is a bit complicated here, but it boils down to the following :
  45. - No more than 5 sounds will be loaded at the same time
  46. - Every sound is getting loaded in a separate thread, so the GUI is not blocked
  47. - We're not waiting using the executor but by waiting for all tasks to end,
  48. so the operation stays asynchronous
  49. """
  50. with self.lock:
  51. self.loaded_sounds = defaultdict(list)
  52. self.nb_max_sounds = self.max_sounds()
  53. executor = ThreadPoolExecutor(max_workers=5)
  54. loop = asyncio.get_running_loop()
  55. tasks: List = []
  56. for category in os.listdir("sounds"):
  57. for file in os.listdir(os.path.join("sounds", category)):
  58. if file.startswith(".git") or file == "desktop.ini":
  59. continue
  60. filepath = os.path.join("sounds", category, file)
  61. tasks.append(
  62. loop.run_in_executor(executor, self.load, category, filepath)
  63. )
  64. executor.shutdown(wait=False)
  65. await asyncio.gather(*tasks)
  66. wx.CallAfter(
  67. self.client.gui.SetStatusText,
  68. f"{self.nb_max_sounds} sounds loaded.",
  69. )
  70. async def _play(self, sound) -> None:
  71. """Play sound from its file path."""
  72. sound = Source(sound)
  73. # gain can be between 0.0 and 2.0 with the GUI's volume slider
  74. gain: float = 0.0 if self.volume == 0 else self.volume / 50.0
  75. sound.set_gain(gain)
  76. sound.play()
  77. while sound.get_state() == AL_PLAYING:
  78. # Don't end the thread until the sound finished playing
  79. await asyncio.sleep(1)
  80. sound.destroy()
  81. def play(self, sound_name: str) -> bool:
  82. """Tries playing a sound by its name.
  83. Returns True if the sound was played successfully.
  84. """
  85. with self.lock:
  86. if len(self.loaded_sounds[sound_name]) > 0:
  87. sound = random.choice(self.loaded_sounds[sound_name])
  88. StartCoroutine(self._play(sound), self.client.gui)
  89. return True
  90. else:
  91. print(f"[!] No sound found for '{sound_name}'.")
  92. return False