Files
Archipelago/worlds/apquest/client/sounds.py
NewSoupVi 88dc135960 APQuest: Various fixes (#6079)
* Import Buffer from typing_extensions instead of collections.abc for 3.11 compat

* always re-set sound volumes before playing

* fix game window scaling if parent is vertical

* make default volume lower
2026-03-30 00:32:06 +02:00

253 lines
8.5 KiB
Python

import asyncio
import pkgutil
from asyncio import Task
from pathlib import Path
from typing import cast
from kivy import Config
from kivy.core.audio import Sound, SoundLoader
from typing_extensions import Buffer
from CommonClient import logger
from .. import game
from .item_quality import ItemQuality
from .utils import make_data_directory
ITEM_JINGLES = {
ItemQuality.PROGUSEFUL: "8bit ProgUseful.ogg",
ItemQuality.PROGRESSION: "8bit Progression.ogg",
ItemQuality.USEFUL: "8bit Useful.ogg",
ItemQuality.TRAP: "8bit Trap.ogg",
ItemQuality.FILLER: "8bit Filler.ogg",
}
CONFETTI_CANNON = "APQuest Confetti Cannon.ogg"
MATH_PROBLEM_STARTED_JINGLE = "APQuest Math Problem Starter Jingle.ogg"
MATH_PROBLEM_SOLVED_JINGLE = "APQuest Math Problem Solved Jingle.ogg"
VICTORY_JINGLE = "8bit Victory.ogg"
ALL_JINGLES = [
MATH_PROBLEM_SOLVED_JINGLE,
MATH_PROBLEM_STARTED_JINGLE,
CONFETTI_CANNON,
VICTORY_JINGLE,
*ITEM_JINGLES.values(),
]
BACKGROUND_MUSIC_INTRO = "APQuest Intro.ogg"
BACKGROUND_MUSIC = "APQuest BGM.ogg"
MATH_TIME_BACKGROUND_MUSIC = "APQuest Math BGM.ogg"
ALL_BGM = [
BACKGROUND_MUSIC_INTRO,
BACKGROUND_MUSIC,
MATH_TIME_BACKGROUND_MUSIC,
]
ALL_SOUNDS = [
*ALL_JINGLES,
*ALL_BGM,
]
class SoundManager:
sound_paths: dict[str, Path]
jingles: dict[str, Sound]
bgm_songs: dict[str, Sound]
active_bgm_song: str = BACKGROUND_MUSIC_INTRO
current_background_music_volume: float = 1.0
background_music_target_volume: float = 0.0
background_music_task: Task[None] | None = None
background_music_last_position: int = 0
volume_percentage: int = 0
game_started: bool
math_trap_active: bool
allow_intro_to_play: bool
def __init__(self) -> None:
self.extract_sounds()
self.populate_sounds()
self.game_started = False
self.allow_intro_to_play = False
self.math_trap_active = False
self.ensure_config()
self.background_music_task = asyncio.create_task(self.sound_manager_loop())
def ensure_config(self) -> None:
Config.adddefaultsection("APQuest")
Config.setdefault("APQuest", "volume", 30)
self.set_volume_percentage(Config.getint("APQuest", "volume"))
async def sound_manager_loop(self) -> None:
while True:
self.update_background_music()
self.do_fade()
await asyncio.sleep(0.02)
def extract_sounds(self) -> None:
# Kivy appears to have no good way of loading audio from bytes.
# So, we have to extract it out of the .apworld first
sound_paths = {}
sound_directory = make_data_directory("sounds")
for sound in ALL_SOUNDS:
sound_file_location = sound_directory / sound
sound_paths[sound] = sound_file_location
if sound_file_location.exists():
continue
with open(sound_file_location, "wb") as sound_file:
data = pkgutil.get_data(game.__name__, f"audio/{sound}")
if data is None:
logger.exception(f"Unable to extract sound {sound} to Archipelago/data")
continue
sound_file.write(cast(Buffer, data))
self.sound_paths = sound_paths
def load_audio(self, sound_filename: str) -> Sound:
audio_path = self.sound_paths[sound_filename]
sound_object = SoundLoader.load(str(audio_path.absolute()))
sound_object.seek(0)
return sound_object
def populate_sounds(self) -> None:
try:
self.jingles = {sound_filename: self.load_audio(sound_filename) for sound_filename in ALL_JINGLES}
except Exception as e:
logger.exception(e)
try:
self.bgm_songs = {sound_filename: self.load_audio(sound_filename) for sound_filename in ALL_BGM}
for bgm_song in self.bgm_songs.values():
bgm_song.loop = True
bgm_song.seek(0)
except Exception as e:
logger.exception(e)
def play_jingle(self, audio_filename: str) -> None:
higher_priority_sound_is_playing = False
for sound_name, sound in self.jingles.items():
if higher_priority_sound_is_playing: # jingles are ordered by priority, lower priority gets eaten
sound.stop()
continue
if sound_name == audio_filename:
sound.volume = self.volume_percentage / 100
sound.play()
self.update_background_music()
higher_priority_sound_is_playing = True
elif sound.state == "play":
higher_priority_sound_is_playing = True
def update_background_music(self) -> None:
self.update_active_song()
if any(sound.state == "play" for sound in self.jingles.values()):
self.play_background_music(False)
else:
if self.math_trap_active:
# Don't fade math trap song, it ends up feeling better
self.play_background_music(True)
else:
self.fade_background_music(True)
def play_background_music(self, play: bool = True) -> None:
if play:
self.background_music_target_volume = 1
self.set_background_music_volume(1)
else:
self.background_music_target_volume = 0
self.set_background_music_volume(0)
def set_background_music_volume(self, volume: float) -> None:
self.current_background_music_volume = volume
for song_filename, song in self.bgm_songs.items():
if song_filename != self.active_bgm_song:
song.volume = 0
continue
song.volume = volume * self.volume_percentage / 100
def fade_background_music(self, fade_in: bool = True) -> None:
if fade_in:
self.background_music_target_volume = 1
else:
self.background_music_target_volume = 0
def set_volume_percentage(self, volume_percentage: float) -> None:
volume_percentage_int = int(volume_percentage)
if self.volume_percentage != volume_percentage:
self.volume_percentage = volume_percentage_int
Config.set("APQuest", "volume", volume_percentage_int)
Config.write()
self.set_background_music_volume(self.current_background_music_volume)
for jingle in self.jingles.values():
jingle.volume = self.volume_percentage / 100
def do_fade(self) -> None:
if self.current_background_music_volume > self.background_music_target_volume:
self.set_background_music_volume(max(0.0, self.current_background_music_volume - 0.02))
if self.current_background_music_volume < self.background_music_target_volume:
self.set_background_music_volume(min(1.0, self.current_background_music_volume + 0.02))
for song_filename, song in self.bgm_songs.items():
if song_filename != self.active_bgm_song:
if song_filename == BACKGROUND_MUSIC:
# It ends up feeling better if this just always continues playing quietly after being started.
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
if self.game_started and song.state == "stop":
song.volume = self.current_background_music_volume * self.volume_percentage / 100
song.play()
song.seek(0)
continue
song.stop()
song.seek(0)
continue
if self.active_bgm_song == BACKGROUND_MUSIC_INTRO and not self.allow_intro_to_play:
song.stop()
song.seek(0)
continue
if self.current_background_music_volume != 0:
if song.state == "stop":
song.volume = self.current_background_music_volume * self.volume_percentage / 100
song.play()
song.seek(0)
def update_active_song(self) -> None:
new_active_song = self.determine_correct_song()
if new_active_song == self.active_bgm_song:
return
self.active_bgm_song = new_active_song
# reevaluate song volumes
self.set_background_music_volume(self.current_background_music_volume)
def determine_correct_song(self) -> str:
if not self.game_started:
return BACKGROUND_MUSIC_INTRO
if self.math_trap_active:
return MATH_TIME_BACKGROUND_MUSIC
return BACKGROUND_MUSIC