diff --git a/Core/Config.cpp b/Core/Config.cpp index e8657bd4bee9..db67ba515b45 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -433,6 +433,7 @@ static ConfigSetting generalSettings[] = { ConfigSetting("Language", &g_Config.sLanguageIni, &DefaultLangRegion), ConfigSetting("ForceLagSync2", &g_Config.bForceLagSync, false, true, true), ConfigSetting("DiscordPresence", &g_Config.bDiscordPresence, true, true, false), // Or maybe it makes sense to have it per-game? Race conditions abound... + ConfigSetting("UISound", &g_Config.bUISound, false, true, false), ReportedConfigSetting("NumWorkerThreads", &g_Config.iNumWorkerThreads, &DefaultNumWorkers, true, true), ConfigSetting("AutoLoadSaveState", &g_Config.iAutoLoadSaveState, 0, true, true), diff --git a/Core/Config.h b/Core/Config.h index d288f6230596..71fa93e3575d 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -196,6 +196,7 @@ struct Config { int iMaxRecent; int iCurrentStateSlot; int iRewindFlipFrequency; + bool bUISound; bool bEnableStateUndo; int iAutoLoadSaveState; // 0 = off, 1 = oldest, 2 = newest, >2 = slot number + 3 bool bEnableCheats; diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp index 70bfb5f509b9..37a3ae9ede64 100644 --- a/Core/HLE/sceMpeg.cpp +++ b/Core/HLE/sceMpeg.cpp @@ -1882,11 +1882,11 @@ static u32 sceMpegAvcCopyYCbCr(u32 mpeg, u32 sourceAddr, u32 YCbCrAddr) MpegContext *ctx = getMpegCtx(mpeg); if (!ctx) { - WARN_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x): bad mpeg handle", mpeg, sourceAddr, YCbCrAddr); + ERROR_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x): bad mpeg handle", mpeg, sourceAddr, YCbCrAddr); return -1; } - ERROR_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x)", mpeg, sourceAddr, YCbCrAddr); + WARN_LOG(ME, "UNIMPL sceMpegAvcCopyYCbCr(%08x, %08x, %08x)", mpeg, sourceAddr, YCbCrAddr); return 0; } diff --git a/Core/WaveFile.cpp b/Core/WaveFile.cpp index f789a7807e78..038c66e80f65 100644 --- a/Core/WaveFile.cpp +++ b/Core/WaveFile.cpp @@ -1,7 +1,7 @@ // Copyright 2008 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. -#ifndef MOBILE_DEVICE + #include #include "Core/WaveFile.h" @@ -105,4 +105,3 @@ void WaveFileWriter::AddStereoSamples(const short* sample_data, u32 count) file.WriteBytes(sample_data, count * 4); audio_size += count * 4; } -#endif diff --git a/Core/WaveFile.h b/Core/WaveFile.h index 39bcf483a475..c2ba17d802e5 100644 --- a/Core/WaveFile.h +++ b/Core/WaveFile.h @@ -11,7 +11,6 @@ // --------------------------------------------------------------------------------- #pragma once -#ifndef MOBILE_DEVICE #include #include @@ -40,6 +39,3 @@ class WaveFileWriter void Write(u32 value); void Write4(const char* ptr); }; - -#endif - diff --git a/UI/BackgroundAudio.cpp b/UI/BackgroundAudio.cpp index f5bdd81da49a..b8a08fcaa9d7 100644 --- a/UI/BackgroundAudio.cpp +++ b/UI/BackgroundAudio.cpp @@ -4,6 +4,8 @@ #include "base/logging.h" #include "base/timeutil.h" #include "file/chunk_file.h" +#include "file/vfs.h" +#include "ui/root.h" #include "Common/CommonTypes.h" #include "Core/HW/SimpleAudioDec.h" @@ -11,173 +13,195 @@ #include "Common/FixedSizeQueue.h" #include "GameInfoCache.h" #include "Core/Config.h" +#include "UI/BackgroundAudio.h" + +struct WavData { + int num_channels = -1; + int sample_rate = -1; + int numFrames = -1; + int samplesPerSec = -1; + int avgBytesPerSec = -1; + int raw_offset_loop_start = 0; + int raw_offset_loop_end = 0; + int loop_start_offset = 0; + int loop_end_offset = 0; + int codec = 0; + int raw_bytes_per_frame = 0; + uint8_t *raw_data = nullptr; + int raw_data_size = 0; + u8 at3_extradata[16]; + + void Read(RIFFReader &riff); + + ~WavData() { + free(raw_data); + raw_data = nullptr; + } +}; -// Really simple looping in-memory AT3 player that also takes care of reading the file format. -// Turns out that AT3 files used for this are modified WAVE files so fairly easy to parse. -class AT3PlusReader { -public: - AT3PlusReader(const std::string &data) - : file_((const uint8_t *)&data[0], (int32_t)data.size()) { - // Normally 8k but let's be safe. - buffer_ = new short[32 * 1024]; - - int codec = PSP_CODEC_AT3PLUS; - u8 at3_extradata[16]; - - int num_channels, sample_rate, numFrames, samplesPerSec, avgBytesPerSec, Nothing; - if (file_.Descend('RIFF')) { - file_.ReadInt(); //get past 'WAVE' - if (file_.Descend('fmt ')) { //enter the format chunk - int temp = file_.ReadInt(); - int format = temp & 0xFFFF; - switch (format) { - case 0xFFFE: - codec = PSP_CODEC_AT3PLUS; - break; - case 0x270: - codec = PSP_CODEC_AT3; - break; - default: - ERROR_LOG(SCEAUDIO, "Unexpected SND0.AT3 format %04x", format); - return; - } - - num_channels = temp >> 16; +void WavData::Read(RIFFReader &file_) { + // If we have no loop start info, we'll just loop the entire audio. + raw_offset_loop_start = 0; + raw_offset_loop_end = 0; + + if (file_.Descend('RIFF')) { + file_.ReadInt(); //get past 'WAVE' + if (file_.Descend('fmt ')) { //enter the format chunk + int temp = file_.ReadInt(); + int format = temp & 0xFFFF; + switch (format) { + case 0xFFFE: + codec = PSP_CODEC_AT3PLUS; + break; + case 0x270: + codec = PSP_CODEC_AT3; + break; + case 1: + // Raw wave data, no codec + codec = 0; + break; + default: + ERROR_LOG(SCEAUDIO, "Unexpected wave format %04x", format); + return; + } - samplesPerSec = file_.ReadInt(); - avgBytesPerSec = file_.ReadInt(); + num_channels = temp >> 16; - temp = file_.ReadInt(); - raw_bytes_per_frame_ = temp & 0xFFFF; - Nothing = temp >> 16; + samplesPerSec = file_.ReadInt(); + /*avgBytesPerSec =*/ file_.ReadInt(); - // Not currently used, but part of the format. - (void)avgBytesPerSec; - (void)Nothing; + temp = file_.ReadInt(); + raw_bytes_per_frame = temp & 0xFFFF; - if (codec == PSP_CODEC_AT3) { - // The first two bytes are actually not a useful part of the extradata. - // We already read 16 bytes, so make sure there's enough left. - if (file_.GetCurrentChunkSize() >= 32) { - file_.ReadData(at3_extradata, 16); - } else { - memset(at3_extradata, 0, sizeof(at3_extradata)); - } + if (codec == PSP_CODEC_AT3) { + // The first two bytes are actually not a useful part of the extradata. + // We already read 16 bytes, so make sure there's enough left. + if (file_.GetCurrentChunkSize() >= 32) { + file_.ReadData(at3_extradata, 16); + } else { + memset(at3_extradata, 0, sizeof(at3_extradata)); } - file_.Ascend(); - // ILOG("got fmt data: %i", samplesPerSec); - } else { - ELOG("Error - no format chunk in wav"); - file_.Ascend(); - return; } + file_.Ascend(); + // ILOG("got fmt data: %i", samplesPerSec); + } else { + ELOG("Error - no format chunk in wav"); + file_.Ascend(); + return; + } - // If we have no loop info, we'll just loop the entire audio. - raw_offset_loop_start_ = 0; - raw_offset_loop_end_ = 0; - skip_next_samples_ = 0; - - if (file_.Descend('smpl')) { - std::vector smplData; - smplData.resize(file_.GetCurrentChunkSize()); - file_.ReadData(&smplData[0], (int)smplData.size()); - - int numLoops = *(int *)&smplData[28]; - struct AtracLoopInfo { - int cuePointID; - int type; - int startSample; - int endSample; - int fraction; - int playCount; - }; - - if (numLoops > 0 && smplData.size() >= 36 + sizeof(AtracLoopInfo) * numLoops) { - AtracLoopInfo *loops = (AtracLoopInfo *)&smplData[36]; - int samplesPerFrame = codec == PSP_CODEC_AT3PLUS ? 2048 : 1024; - - for (int i = 0; i < numLoops; ++i) { - // Only seen forward loops, so let's ignore others. - if (loops[i].type != 0) - continue; - - // We ignore loop interpolation (fraction) and play count for now. - raw_offset_loop_start_ = (loops[i].startSample / samplesPerFrame) * raw_bytes_per_frame_; - loop_start_offset_ = loops[i].startSample % samplesPerFrame; - raw_offset_loop_end_ = (loops[i].endSample / samplesPerFrame) * raw_bytes_per_frame_; - loop_end_offset_ = loops[i].endSample % samplesPerFrame; - - if (loops[i].playCount == 0) { - // This was an infinite loop, so ignore the rest. - // In practice, there's usually only one and it's usually infinite. - break; - } + if (file_.Descend('smpl')) { + std::vector smplData; + smplData.resize(file_.GetCurrentChunkSize()); + file_.ReadData(&smplData[0], (int)smplData.size()); + + int numLoops = *(int *)&smplData[28]; + struct AtracLoopInfo { + int cuePointID; + int type; + int startSample; + int endSample; + int fraction; + int playCount; + }; + + if (numLoops > 0 && smplData.size() >= 36 + sizeof(AtracLoopInfo) * numLoops) { + AtracLoopInfo *loops = (AtracLoopInfo *)&smplData[36]; + int samplesPerFrame = codec == PSP_CODEC_AT3PLUS ? 2048 : 1024; + + for (int i = 0; i < numLoops; ++i) { + // Only seen forward loops, so let's ignore others. + if (loops[i].type != 0) + continue; + + // We ignore loop interpolation (fraction) and play count for now. + raw_offset_loop_start = (loops[i].startSample / samplesPerFrame) * raw_bytes_per_frame; + loop_start_offset = loops[i].startSample % samplesPerFrame; + raw_offset_loop_end = (loops[i].endSample / samplesPerFrame) * raw_bytes_per_frame; + loop_end_offset = loops[i].endSample % samplesPerFrame; + + if (loops[i].playCount == 0) { + // This was an infinite loop, so ignore the rest. + // In practice, there's usually only one and it's usually infinite. + break; } } - - file_.Ascend(); } - if (file_.Descend('data')) { //enter the data chunk - int numBytes = file_.GetCurrentChunkSize(); - numFrames = numBytes / raw_bytes_per_frame_; // numFrames + file_.Ascend(); + } - raw_data_ = (uint8_t *)malloc(numBytes); - raw_data_size_ = numBytes; - if (num_channels == 1 || num_channels == 2) { - file_.ReadData(raw_data_, numBytes); - } else { - ELOG("Error - bad blockalign or channels"); - free(raw_data_); - raw_data_ = nullptr; - return; - } - file_.Ascend(); + // enter the data chunk + if (file_.Descend('data')) { + int numBytes = file_.GetCurrentChunkSize(); + numFrames = numBytes / raw_bytes_per_frame; // numFrames + + raw_data = (uint8_t *)malloc(numBytes); + raw_data_size = numBytes; + if (num_channels == 1 || num_channels == 2) { + file_.ReadData(raw_data, numBytes); } else { - ELOG("Error - no data chunk in wav"); - file_.Ascend(); + ELOG("Error - bad blockalign or channels"); + free(raw_data); + raw_data = nullptr; return; } file_.Ascend(); } else { - ELOG("Could not descend into RIFF file. Data size=%d", (int32_t)data.size()); + ELOG("Error - no data chunk in wav"); + file_.Ascend(); return; } - sample_rate = samplesPerSec; - decoder_ = new SimpleAudio(codec, sample_rate, num_channels); - if (codec == PSP_CODEC_AT3) { - decoder_->SetExtraData(&at3_extradata[2], 14, raw_bytes_per_frame_); - } - ILOG("read ATRAC, frames: %i, rate %i", numFrames, sample_rate); + file_.Ascend(); + } else { + ELOG("Could not descend into RIFF file."); + return; } + sample_rate = samplesPerSec; +} - ~AT3PlusReader() { +// Really simple looping in-memory AT3 player that also takes care of reading the file format. +// Turns out that AT3 files used for this are modified WAVE files so fairly easy to parse. +class AT3PlusReader { +public: + explicit AT3PlusReader(const std::string &data) + : file_((const uint8_t *)&data[0], (int32_t)data.size()) { + // Normally 8k but let's be safe. + buffer_ = new short[32 * 1024]; + + skip_next_samples_ = 0; + + wave_.Read(file_); + + decoder_ = new SimpleAudio(wave_.codec, wave_.sample_rate, wave_.num_channels); + if (wave_.codec == PSP_CODEC_AT3) { + decoder_->SetExtraData(&wave_.at3_extradata[2], 14, wave_.raw_bytes_per_frame); + } + ILOG("read ATRAC, frames: %d, rate %d", wave_.numFrames, wave_.sample_rate); } - void Shutdown() { - free(raw_data_); - raw_data_ = nullptr; + ~AT3PlusReader() { delete[] buffer_; buffer_ = nullptr; delete decoder_; decoder_ = nullptr; } - bool IsOK() { return raw_data_ != nullptr; } + bool IsOK() { return wave_.raw_data != nullptr; } bool Read(int *buffer, int len) { - if (!raw_data_) + if (!wave_.raw_data) return false; while (bgQueue.size() < (size_t)(len * 2)) { int outBytes = 0; - decoder_->Decode(raw_data_ + raw_offset_, raw_bytes_per_frame_, (uint8_t *)buffer_, &outBytes); + decoder_->Decode(wave_.raw_data + raw_offset_, wave_.raw_bytes_per_frame, (uint8_t *)buffer_, &outBytes); if (!outBytes) return false; - if (raw_offset_loop_end_ != 0 && raw_offset_ == raw_offset_loop_end_) { + if (wave_.raw_offset_loop_end != 0 && raw_offset_ == wave_.raw_offset_loop_end) { // Only take the remaining bytes, but convert to stereo s16. - outBytes = std::min(outBytes, loop_end_offset_ * 4); + outBytes = std::min(outBytes, wave_.loop_end_offset * 4); } int start = skip_next_samples_; @@ -187,16 +211,16 @@ class AT3PlusReader { bgQueue.push(buffer_[i]); } - if (raw_offset_loop_end_ != 0 && raw_offset_ == raw_offset_loop_end_) { + if (wave_.raw_offset_loop_end != 0 && raw_offset_ == wave_.raw_offset_loop_end) { // Time to loop. Account for the addition below. - raw_offset_ = raw_offset_loop_start_ - raw_bytes_per_frame_; + raw_offset_ = wave_.raw_offset_loop_start - wave_.raw_bytes_per_frame; // This time we're counting each stereo sample. - skip_next_samples_ = loop_start_offset_ * 2; + skip_next_samples_ = wave_.loop_start_offset * 2; } // Handle loops when there's no loop info. - raw_offset_ += raw_bytes_per_frame_; - if (raw_offset_ >= raw_data_size_) { + raw_offset_ += wave_.raw_bytes_per_frame; + if (raw_offset_ >= wave_.raw_data_size) { raw_offset_ = 0; } } @@ -209,123 +233,181 @@ class AT3PlusReader { private: RIFFReader file_; - uint8_t *raw_data_ = nullptr; - int raw_data_size_ = 0; + + WavData wave_; + int raw_offset_ = 0; - int raw_bytes_per_frame_; - int raw_offset_loop_start_ = 0; - int raw_offset_loop_end_ = 0; - int loop_start_offset_ = 0; - int loop_end_offset_ = 0; int skip_next_samples_ = 0; FixedSizeQueue bgQueue; short *buffer_ = nullptr; SimpleAudio *decoder_ = nullptr; }; -static std::mutex g_bgMutex; -static std::string bgGamePath; -static int playbackOffset; -static AT3PlusReader *at3Reader; -static double gameLastChanged; -static double lastPlaybackTime; -static int buffer[44100]; -static bool fadingOut = true; -static float volume; -static float delta = -0.0001f; - -static void ClearBackgroundAudio(bool hard) { +BackgroundAudio g_BackgroundAudio; + +BackgroundAudio::BackgroundAudio() { + buffer = new int[BUFSIZE](); +} + +BackgroundAudio::~BackgroundAudio() { + delete[] buffer; +} + +BackgroundAudio::Sample *BackgroundAudio::LoadSample(const std::string &path) { + size_t bytes; + uint8_t *data = VFSReadFile(path.c_str(), &bytes); + if (!data) { + return nullptr; + } + + RIFFReader reader(data, (int)bytes); + + WavData wave; + wave.Read(reader); + + if (wave.num_channels != 2 || wave.sample_rate != 44100 || wave.raw_bytes_per_frame != 4) { + ELOG("Wave format not supported for mixer playback. Must be 16-bit raw stereo. '%s'", path.c_str()); + return nullptr; + } + + int16_t *samples = new int16_t[2 * wave.numFrames]; + memcpy(samples, wave.raw_data, wave.numFrames * wave.raw_bytes_per_frame); + + return new BackgroundAudio::Sample(samples, wave.numFrames); +} + +void BackgroundAudio::LoadSamples() { + samples_.resize((size_t)UI::UISound::COUNT); + samples_[(size_t)UI::UISound::BACK] = std::unique_ptr(LoadSample("sfx_back.wav")); + samples_[(size_t)UI::UISound::SELECT] = std::unique_ptr(LoadSample("sfx_select.wav")); + samples_[(size_t)UI::UISound::CONFIRM] = std::unique_ptr(LoadSample("sfx_confirm.wav")); + samples_[(size_t)UI::UISound::TOGGLE_ON] = std::unique_ptr(LoadSample("sfx_toggle_on.wav")); + samples_[(size_t)UI::UISound::TOGGLE_OFF] = std::unique_ptr(LoadSample("sfx_toggle_off.wav")); + + UI::SetSoundCallback([](UI::UISound sound) { + g_BackgroundAudio.PlaySFX(sound); + }); +} + +void BackgroundAudio::PlaySFX(UI::UISound sfx) { + std::lock_guard lock(mutex_); + plays_.push_back(PlayInstance{ sfx, 0, 64, false }); +} + +void BackgroundAudio::Clear(bool hard) { if (!hard) { - fadingOut = true; - volume = 1.0f; + fadingOut_ = true; + volume_ = 1.0f; return; } - if (at3Reader) { - at3Reader->Shutdown(); - delete at3Reader; - at3Reader = nullptr; + if (at3Reader_) { + delete at3Reader_; + at3Reader_ = nullptr; } - playbackOffset = 0; + playbackOffset_ = 0; } -void SetBackgroundAudioGame(const std::string &path) { +void BackgroundAudio::SetGame(const std::string &path) { time_update(); - std::lock_guard lock(g_bgMutex); - if (path == bgGamePath) { + std::lock_guard lock(mutex_); + if (path == bgGamePath_) { // Do nothing return; } if (path.size() == 0) { - ClearBackgroundAudio(false); - fadingOut = true; + Clear(false); + fadingOut_ = true; } else { - ClearBackgroundAudio(true); - gameLastChanged = time_now_d(); - fadingOut = false; + Clear(true); + gameLastChanged_ = time_now_d(); + fadingOut_ = false; } - volume = 1.0f; - bgGamePath = path; + volume_ = 1.0f; + bgGamePath_ = path; } -int PlayBackgroundAudio() { +int BackgroundAudio::Play() { time_update(); - std::lock_guard lock(g_bgMutex); + std::lock_guard lock(mutex_); // Immediately stop the sound if it is turned off while playing. if (!g_Config.bEnableSound) { - ClearBackgroundAudio(true); + Clear(true); __PushExternalAudio(0, 0); return 0; } - double now = time_now(); - if (at3Reader) { - int sz = lastPlaybackTime <= 0.0 ? 44100 / 60 : (int)((now - lastPlaybackTime) * 44100); - sz = std::min((int)ARRAY_SIZE(buffer) / 2, sz); + double now = time_now_d(); + int sz = lastPlaybackTime_ <= 0.0 ? 44100 / 60 : (int)((now - lastPlaybackTime_) * 44100); + sz = std::min(BUFSIZE / 2, sz); + if (at3Reader_) { if (sz >= 16) { - if (at3Reader->Read(buffer, sz)) { - if (!fadingOut) { - __PushExternalAudio(buffer, sz); - } else { + if (at3Reader_->Read(buffer, sz)) { + if (fadingOut_) { for (int i = 0; i < sz*2; i += 2) { - buffer[i] *= volume; - buffer[i + 1] *= volume; - volume += delta; - } - __PushExternalAudio(buffer, sz); - if (volume <= 0.0f) { - ClearBackgroundAudio(true); - fadingOut = false; - gameLastChanged = 0; + buffer[i] *= volume_; + buffer[i + 1] *= volume_; + volume_ += delta_; } } } - lastPlaybackTime = now; } } else { - __PushExternalAudio(0, 0); - lastPlaybackTime = now; + for (int i = 0; i < sz * 2; i += 2) { + buffer[i] = 0; + buffer[i + 1] = 0; + } } + // Mix in menu sound effects. Terribly slow mixer but meh. + if (!plays_.empty()) { + for (int i = 0; i < sz * 2; i += 2) { + std::vector::iterator iter = plays_.begin(); + while (iter != plays_.end()) { + PlayInstance inst = *iter; + auto sample = samples_[(int)inst.sound].get(); + if (!sample || iter->offset >= sample->length_) { + iter->done = true; + iter = plays_.erase(iter); + } else { + if (!iter->done) { + buffer[i] += sample->data_[inst.offset * 2] * inst.volume >> 8; + buffer[i + 1] += sample->data_[inst.offset * 2 + 1] * inst.volume >> 8; + } + iter->offset++; + iter++; + } + } + } + } + + __PushExternalAudio(buffer, sz); + + if (at3Reader_ && fadingOut_ && volume_ <= 0.0f) { + Clear(true); + fadingOut_ = false; + gameLastChanged_ = 0; + } + + lastPlaybackTime_ = now; + return 0; } -// Stuff that should be on the UI thread only, like anything to do with -// g_gameInfoCache. -void UpdateBackgroundAudio() { +void BackgroundAudio::Update() { // If there's a game, and some time has passed since the selected game // last changed... (to prevent crazy amount of reads when skipping through a list) - if (bgGamePath.size() && (time_now_d() - gameLastChanged > 0.5)) { - std::lock_guard lock(g_bgMutex); - if (!at3Reader) { + if (bgGamePath_.size() && (time_now_d() - gameLastChanged_ > 0.5)) { + std::lock_guard lock(mutex_); + if (!at3Reader_) { // Grab some audio from the current game and play it. if (!g_gameInfoCache) return; - std::shared_ptr gameInfo = g_gameInfoCache->GetInfo(NULL, bgGamePath, GAMEINFO_WANTSND); + std::shared_ptr gameInfo = g_gameInfoCache->GetInfo(NULL, bgGamePath_, GAMEINFO_WANTSND); if (!gameInfo) return; @@ -336,8 +418,8 @@ void UpdateBackgroundAudio() { if (gameInfo->sndFileData.size()) { const std::string &data = gameInfo->sndFileData; - at3Reader = new AT3PlusReader(data); - lastPlaybackTime = 0.0; + at3Reader_ = new AT3PlusReader(data); + lastPlaybackTime_ = 0.0; } } } diff --git a/UI/BackgroundAudio.h b/UI/BackgroundAudio.h index 5d9ae1e040c6..a0c154411e24 100644 --- a/UI/BackgroundAudio.h +++ b/UI/BackgroundAudio.h @@ -1,7 +1,63 @@ #pragma once #include +#include +#include -void SetBackgroundAudioGame(const std::string &path); -int PlayBackgroundAudio(); -void UpdateBackgroundAudio(); +#include "ui/root.h" + +class AT3PlusReader; + +class BackgroundAudio { +public: + BackgroundAudio(); + ~BackgroundAudio(); + + void Clear(bool hard); + void SetGame(const std::string &path); + void Update(); + int Play(); + + void LoadSamples(); + void PlaySFX(UI::UISound sfx); + +private: + enum { + BUFSIZE = 44100, + }; + + std::mutex mutex_; + std::string bgGamePath_; + int playbackOffset_ = 0; + AT3PlusReader *at3Reader_; + double gameLastChanged_ = 0.0; + double lastPlaybackTime_ = 0.0; + int *buffer = nullptr; + bool fadingOut_ = true; + float volume_ = 0.0f; + float delta_ = -0.0001f; + + struct PlayInstance { + UI::UISound sound; + int offset; + int volume; // 0..255 + bool done; + }; + + struct Sample { + // data must be new-ed. + Sample(int16_t *data, int length) : data_(data), length_(length) {} + ~Sample() { + delete[] data_; + } + int16_t *data_; + int length_; // stereo samples. + }; + + static Sample *LoadSample(const std::string &path); + + std::vector plays_; + std::vector> samples_; +}; + +extern BackgroundAudio g_BackgroundAudio; diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index b517d6c206cf..a2b476fa2561 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -193,7 +193,7 @@ void EmuScreen::bootGame(const std::string &filename) { return; } - SetBackgroundAudioGame(""); + g_BackgroundAudio.SetGame(""); // Check permission status first, in case we came from a shortcut. if (!bootAllowStorage(filename)) diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index 1c900beecdd1..6b4f0b05e1cb 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -38,7 +38,7 @@ #include "UI/BackgroundAudio.h" GameScreen::GameScreen(const std::string &gamePath) : UIDialogScreenWithGameBackground(gamePath) { - SetBackgroundAudioGame(gamePath); + g_BackgroundAudio.SetGame(gamePath); } GameScreen::~GameScreen() { diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 7c6637faef68..e0c8d2750f2e 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -816,8 +816,9 @@ void GameSettingsScreen::CreateViews() { systemSettingsScroll->Add(systemSettings); tabHolder->AddTab(ms->T("System"), systemSettingsScroll); - systemSettings->Add(new ItemHeader(sy->T("UI Language"))); + systemSettings->Add(new ItemHeader(sy->T("UI Language"))); // Should be renamed "UI"? systemSettings->Add(new Choice(dev->T("Language", "Language")))->OnClick.Handle(this, &GameSettingsScreen::OnLanguage); + systemSettings->Add(new CheckBox(&g_Config.bUISound, dev->T("UI Sound"))); systemSettings->Add(new ItemHeader(sy->T("Help the PPSSPP team"))); enableReports_ = Reporting::IsEnabled(); @@ -859,7 +860,6 @@ void GameSettingsScreen::CreateViews() { } } #endif - systemSettings->Add(new CheckBox(&g_Config.bCheckForNewVersion, sy->T("VersionCheck", "Check for new versions of PPSSPP"))); const std::string bgPng = GetSysDirectory(DIRECTORY_SYSTEM) + "background.png"; const std::string bgJpg = GetSysDirectory(DIRECTORY_SYSTEM) + "background.jpg"; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index e9803ac0f63e..c44456821408 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -935,12 +935,12 @@ UI::EventReturn GameBrowser::OnHomebrewStore(UI::EventParams &e) { MainScreen::MainScreen() { System_SendMessage("event", "mainscreen"); - SetBackgroundAudioGame(""); + g_BackgroundAudio.SetGame(""); lastVertical_ = UseVerticalLayout(); } MainScreen::~MainScreen() { - SetBackgroundAudioGame(""); + g_BackgroundAudio.SetGame(""); } void MainScreen::CreateViews() { @@ -1283,7 +1283,7 @@ UI::EventReturn MainScreen::OnGameSelected(UI::EventParams &e) { // Restore focus if it was highlighted (e.g. by gamepad.) restoreFocusGamePath_ = highlightedGamePath_; - SetBackgroundAudioGame(path); + g_BackgroundAudio.SetGame(path); lockBackgroundAudio_ = true; screenManager()->push(new GameScreen(path)); return UI::EVENT_DONE; @@ -1314,7 +1314,7 @@ UI::EventReturn MainScreen::OnGameHighlight(UI::EventParams &e) { } if ((!highlightedGamePath_.empty() || e.a == FF_LOSTFOCUS) && !lockBackgroundAudio_) { - SetBackgroundAudioGame(highlightedGamePath_); + g_BackgroundAudio.SetGame(highlightedGamePath_); } lockBackgroundAudio_ = false; @@ -1401,7 +1401,7 @@ void MainScreen::dialogFinished(const Screen *dialog, DialogResult result) { restoreFocusGamePath_.clear(); } else { // Not refocusing, so we need to stop the audio. - SetBackgroundAudioGame(""); + g_BackgroundAudio.SetGame(""); } } } diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 1689e333cdff..4c222078faf9 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -271,7 +271,7 @@ std::string NativeQueryConfig(std::string query) { int NativeMix(short *audio, int num_samples) { if (GetUIState() != UISTATE_INGAME) { - PlayBackgroundAudio(); + g_BackgroundAudio.Play(); } int sample_rate = System_GetPropertyInt(SYSPROP_AUDIO_SAMPLE_RATE); @@ -446,6 +446,9 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch g_Discord.SetPresenceMenu(); + // TODO: Load these in the background instead of synchronously. + g_BackgroundAudio.LoadSamples(); + // Make sure UI state is MENU. ResetUIState(); @@ -1040,7 +1043,7 @@ void NativeRender(GraphicsContext *graphicsContext) { if (GetUIState() != UISTATE_INGAME) { // Note: We do this from NativeRender so that the graphics context is // guaranteed valid, to be safe - g_gameInfoCache messes around with textures. - UpdateBackgroundAudio(); + g_BackgroundAudio.Update(); } float xres = dp_xres; @@ -1205,6 +1208,8 @@ void NativeUpdate() { screenManager->update(); g_Discord.Update(); + + UI::SetSoundEnabled(g_Config.bUISound); } bool NativeIsAtTopLevel() { diff --git a/assets/sfx_back.wav b/assets/sfx_back.wav new file mode 100644 index 000000000000..85ae898c11a4 Binary files /dev/null and b/assets/sfx_back.wav differ diff --git a/assets/sfx_confirm.wav b/assets/sfx_confirm.wav new file mode 100644 index 000000000000..87d7b4eb2bae Binary files /dev/null and b/assets/sfx_confirm.wav differ diff --git a/assets/sfx_select.wav b/assets/sfx_select.wav new file mode 100644 index 000000000000..3e65be69a521 Binary files /dev/null and b/assets/sfx_select.wav differ diff --git a/assets/sfx_toggle_off.wav b/assets/sfx_toggle_off.wav new file mode 100644 index 000000000000..bab33853f83a Binary files /dev/null and b/assets/sfx_toggle_off.wav differ diff --git a/assets/sfx_toggle_on.wav b/assets/sfx_toggle_on.wav new file mode 100644 index 000000000000..74f9ff222057 Binary files /dev/null and b/assets/sfx_toggle_on.wav differ diff --git a/ext/native/file/zip_read.cpp b/ext/native/file/zip_read.cpp index 5069cfc223de..8c128602461e 100644 --- a/ext/native/file/zip_read.cpp +++ b/ext/native/file/zip_read.cpp @@ -285,6 +285,7 @@ static bool IsLocalPath(const char *path) { return isUnixLocal || isWindowsLocal; } +// The returned data should be free'd with delete[]. uint8_t *VFSReadFile(const char *filename, size_t *size) { if (IsLocalPath(filename)) { // Local path, not VFS. diff --git a/ext/native/file/zip_read.h b/ext/native/file/zip_read.h index 3607ddb926cc..750a71a15402 100644 --- a/ext/native/file/zip_read.h +++ b/ext/native/file/zip_read.h @@ -48,7 +48,7 @@ class ZipAssetReader : public AssetReader { class DirectoryAssetReader : public AssetReader { public: - DirectoryAssetReader(const char *path) { + explicit DirectoryAssetReader(const char *path) { strncpy(path_, path, ARRAY_SIZE(path_)); path_[ARRAY_SIZE(path_) - 1] = '\0'; } @@ -61,6 +61,6 @@ class DirectoryAssetReader : public AssetReader { } private: - char path_[512]; + char path_[512]{}; }; diff --git a/ext/native/ui/root.cpp b/ext/native/ui/root.cpp index ef44036747c6..b71641895e99 100644 --- a/ext/native/ui/root.cpp +++ b/ext/native/ui/root.cpp @@ -2,6 +2,7 @@ #include #include "ppsspp_config.h" + #include "base/timeutil.h" #include "ui/root.h" #include "ui/viewgroup.h" @@ -17,6 +18,10 @@ static bool focusMovementEnabled; bool focusForced; static std::mutex eventMutex_; +static std::function soundCallback; +static bool soundEnabled = true; + + struct DispatchQueueItem { Event *e; EventParams params; @@ -133,6 +138,22 @@ void MoveFocus(ViewGroup *root, FocusDirection direction) { if (neigh.view) { neigh.view->SetFocus(); root->SubviewFocused(neigh.view); + + PlayUISound(UISound::SELECT); + } +} + +void SetSoundEnabled(bool enabled) { + soundEnabled = enabled; +} + +void SetSoundCallback(std::function func) { + soundCallback = func; +} + +void PlayUISound(UISound sound) { + if (soundEnabled && soundCallback) { + soundCallback(sound); } } diff --git a/ext/native/ui/root.h b/ext/native/ui/root.h index 8cfca36f29c2..0944373e274b 100644 --- a/ext/native/ui/root.h +++ b/ext/native/ui/root.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "ui/ui_context.h" #include "input/input_state.h" @@ -26,4 +28,18 @@ bool KeyEvent(const KeyInput &key, ViewGroup *root); bool TouchEvent(const TouchInput &touch, ViewGroup *root); bool AxisEvent(const AxisInput &axis, ViewGroup *root); +enum class UISound { + SELECT = 0, + BACK, + CONFIRM, + TOGGLE_ON, + TOGGLE_OFF, + COUNT, +}; + +void SetSoundEnabled(bool enabled); +void SetSoundCallback(std::function func); + +void PlayUISound(UISound sound); + } // namespace UI diff --git a/ext/native/ui/ui_screen.cpp b/ext/native/ui/ui_screen.cpp index 0a2a665e346b..e2dba2cf60ed 100644 --- a/ext/native/ui/ui_screen.cpp +++ b/ext/native/ui/ui_screen.cpp @@ -168,6 +168,7 @@ bool UIDialogScreen::key(const KeyInput &key) { } else { finished_ = true; TriggerFinish(DR_BACK); + UI::PlayUISound(UI::UISound::BACK); } return true; } diff --git a/ext/native/ui/view.cpp b/ext/native/ui/view.cpp index d452b6a8a375..577fd91e1c79 100644 --- a/ext/native/ui/view.cpp +++ b/ext/native/ui/view.cpp @@ -376,6 +376,7 @@ bool StickyChoice::Key(const KeyInput &key) { if (key.flags & KEY_DOWN) { if (IsAcceptKey(key)) { down_ = true; + UI::PlayUISound(UI::UISound::TOGGLE_ON); Click(); return true; } @@ -424,6 +425,11 @@ void ClickableItem::Draw(UIContext &dc) { DrawBG(dc, style); } +void Choice::Click() { + ClickableItem::Click(); + UI::PlayUISound(UI::UISound::CONFIRM); +} + void Choice::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const { if (atlasImage_.isValid()) { dc.Draw()->GetAtlas()->measureImage(atlasImage_, &w, &h); @@ -596,8 +602,10 @@ void PopupHeader::Draw(UIContext &dc) { } void CheckBox::Toggle() { - if (toggle_) + if (toggle_) { *toggle_ = !(*toggle_); + UI::PlayUISound(*toggle_ ? UI::UISound::TOGGLE_ON : UI::UISound::TOGGLE_OFF); + } } bool CheckBox::Toggled() const { @@ -668,8 +676,14 @@ void CheckBox::GetContentDimensions(const UIContext &dc, float &w, float &h) con } void BitCheckBox::Toggle() { - if (bitfield_) + if (bitfield_) { *bitfield_ = *bitfield_ ^ bit_; + if (*bitfield_ & bit_) { + UI::PlayUISound(UI::UISound::TOGGLE_ON); + } else { + UI::PlayUISound(UI::UISound::TOGGLE_OFF); + } + } } bool BitCheckBox::Toggled() const { @@ -689,6 +703,11 @@ void Button::GetContentDimensions(const UIContext &dc, float &w, float &h) const h += paddingH_; } +void Button::Click() { + Clickable::Click(); + UI::PlayUISound(UI::UISound::CONFIRM); +} + void Button::Draw(UIContext &dc) { Style style = dc.theme->buttonStyle; diff --git a/ext/native/ui/view.h b/ext/native/ui/view.h index 5cf1ad275cd0..335ce822e85e 100644 --- a/ext/native/ui/view.h +++ b/ext/native/ui/view.h @@ -519,6 +519,7 @@ class Button : public Clickable { Button(const std::string &text, ImageID imageID, LayoutParams *layoutParams = 0) : Clickable(layoutParams), text_(text), imageID_(imageID) {} + void Click() override; void Draw(UIContext &dc) override; void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; const std::string &GetText() const { return text_; } @@ -647,6 +648,7 @@ class Choice : public ClickableItem { Choice(ImageID image, LayoutParams *layoutParams = nullptr) : ClickableItem(layoutParams), atlasImage_(image), iconImage_(ImageID::invalid()), centered_(false), highlighted_(false), selected_(false) {} + virtual void Click(); virtual void HighlightChanged(bool highlighted); void GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const override; void Draw(UIContext &dc) override; diff --git a/ext/native/ui/viewgroup.cpp b/ext/native/ui/viewgroup.cpp index 7d188743f0e0..e9f2f8817648 100644 --- a/ext/native/ui/viewgroup.cpp +++ b/ext/native/ui/viewgroup.cpp @@ -9,6 +9,7 @@ #include "math/curves.h" #include "ui/ui_context.h" #include "ui/ui_tween.h" +#include "ui/root.h" #include "ui/view.h" #include "ui/viewgroup.h" #include "gfx_es2/draw_buffer.h" @@ -1321,11 +1322,17 @@ void ChoiceStrip::HighlightChoice(unsigned int choice){ bool ChoiceStrip::Key(const KeyInput &input) { bool ret = false; if (input.flags & KEY_DOWN) { - if (IsTabLeftKey(input) && selected_ > 0) { - SetSelection(selected_ - 1); + if (IsTabLeftKey(input)) { + if (selected_ > 0) { + SetSelection(selected_ - 1); + UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point? + } ret = true; - } else if (IsTabRightKey(input) && selected_ < (int)views_.size() - 1) { - SetSelection(selected_ + 1); + } else if (IsTabRightKey(input)) { + if (selected_ < (int)views_.size() - 1) { + SetSelection(selected_ + 1); + UI::PlayUISound(UI::UISound::TOGGLE_ON); + } ret = true; } }