// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // Sound.cpp // // Higher-level audio functions. // Navigation sounds, audio sample playback etc. // #include "Sound.h" #include "AudioManager.h" #include "Log.h" #include "Settings.h" #include "ThemeData.h" #include "resources/ResourceManager.h" std::shared_ptr Sound::get(const std::string& path) { auto it = sMap.find(path); if (it != sMap.cend()) return it->second; std::shared_ptr sound {std::shared_ptr(new Sound(path))}; AudioManager::getInstance().registerSound(sound); sMap[path] = sound; return sound; } std::shared_ptr Sound::getFromTheme(ThemeData* const theme, const std::string& view, const std::string& element) { std::string elemName {element.substr(6, std::string::npos)}; if (theme == nullptr) { LOG(LogDebug) << "Sound::getFromTheme(): Using fallback sound file for \"" << elemName << "\""; return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + elemName + ".wav")); } LOG(LogDebug) << "Sound::getFromTheme(): Looking for tag "; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "sound")}; if (!elem || !elem->has("path")) { LOG(LogDebug) << "Sound::getFromTheme(): Tag not found, using fallback sound file"; return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + elemName + ".wav")); } if (!Utils::FileSystem::exists(elem->get("path"))) { LOG(LogError) << "Sound::getFromTheme(): Navigation sound tag found but sound file does " "not exist, falling back to default sound"; return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + elemName + ".wav")); } LOG(LogDebug) << "Sound::getFromTheme(): Tag found, ready to load theme sound file"; return get(elem->get("path")); } Sound::Sound(const std::string& path) : mSampleData(nullptr) , mSamplePos(0) , mSampleLength(0) , mPlaying(false) { loadFile(path); } void Sound::loadFile(const std::string& path) { mPath = path; init(); } void Sound::init() { if (mSampleData != nullptr) deinit(); if (mPath.empty()) return; // Load WAV file via SDL. SDL_AudioSpec wave; Uint8* data {nullptr}; Uint32 dlen {0}; if (SDL_LoadWAV(mPath.c_str(), &wave, &data, &dlen) == nullptr) { LOG(LogError) << "Failed to load theme navigation sound file: " << SDL_GetError(); return; } // Convert sound file to the format required by ES-DE. SDL_AudioStream* conversionStream { SDL_NewAudioStream(wave.format, wave.channels, wave.freq, AudioManager::sAudioFormat.format, AudioManager::sAudioFormat.channels, AudioManager::sAudioFormat.freq)}; if (conversionStream == nullptr) { LOG(LogError) << "Failed to create sample conversion stream: " << SDL_GetError(); return; } if (SDL_AudioStreamPut(conversionStream, data, dlen) == -1) { LOG(LogError) << "Failed to put samples in the conversion stream: " << SDL_GetError(); SDL_FreeAudioStream(conversionStream); return; } int sampleLength {SDL_AudioStreamAvailable(conversionStream)}; Uint8* converted {new Uint8[sampleLength]}; if (SDL_AudioStreamGet(conversionStream, converted, sampleLength) == -1) { LOG(LogError) << "Failed to convert sound file '" << mPath << "': " << SDL_GetError(); SDL_FreeAudioStream(conversionStream); delete[] converted; return; } mSampleData = converted; mSampleLength = sampleLength; mSamplePos = 0; mSampleFormat.freq = AudioManager::sAudioFormat.freq; mSampleFormat.channels = AudioManager::sAudioFormat.channels; mSampleFormat.format = AudioManager::sAudioFormat.format; SDL_FreeAudioStream(conversionStream); SDL_FreeWAV(data); } void Sound::deinit() { mPlaying = false; if (mSampleData != nullptr) { SDL_LockAudioDevice(AudioManager::sAudioDevice); delete[] mSampleData; mSampleData = nullptr; mSampleLength = 0; mSamplePos = 0; SDL_UnlockAudioDevice(AudioManager::sAudioDevice); sMap.erase(mPath); } } void Sound::play() { if (mSampleData == nullptr) return; if (!Settings::getInstance()->getBool("NavigationSounds")) return; if (!AudioManager::getInstance().getHasAudioDevice()) return; SDL_LockAudioDevice(AudioManager::sAudioDevice); if (mPlaying) // Replay from start. rewind the sample to the beginning. mSamplePos = 0; else // Flag our sample as playing. mPlaying = true; SDL_UnlockAudioDevice(AudioManager::sAudioDevice); // Tell the AudioManager to start playing samples. AudioManager::getInstance().play(); } void Sound::stop() { // Flag our sample as not playing and rewind its position. SDL_LockAudioDevice(AudioManager::sAudioDevice); mPlaying = false; mSamplePos = 0; SDL_UnlockAudioDevice(AudioManager::sAudioDevice); } void Sound::setPosition(Uint32 newPosition) { mSamplePos = newPosition; if (mSamplePos >= mSampleLength) { // Got to or beyond the end of the sample. stop playing. mPlaying = false; mSamplePos = 0; } } NavigationSounds& NavigationSounds::getInstance() { static NavigationSounds instance; return instance; } void NavigationSounds::deinit() { for (auto sound : mNavigationSounds) { AudioManager::getInstance().unregisterSound(sound); sound->deinit(); } mNavigationSounds.clear(); } void NavigationSounds::loadThemeNavigationSounds(ThemeData* const theme) { if (theme) { LOG(LogDebug) << "NavigationSounds::loadThemeNavigationSounds(): " "Theme includes navigation sound support, loading custom sounds"; } else { LOG(LogDebug) << "NavigationSounds::loadThemeNavigationSounds(): " "Theme does not include navigation sound support, using fallback sounds"; } mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_systembrowse")); mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_quicksysselect")); mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_select")); mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_back")); mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_scroll")); mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_favorite")); mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_launch")); } void NavigationSounds::playThemeNavigationSound(NavigationSoundsID soundID) { NavigationSounds::getInstance().mNavigationSounds[soundID]->play(); } bool NavigationSounds::isPlayingThemeNavigationSound(NavigationSoundsID soundID) { return NavigationSounds::getInstance().mNavigationSounds[soundID]->isPlaying(); }