diff --git a/PCSX2_suite.sln b/PCSX2_suite.sln index bb4791149c..28ddd22c11 100644 --- a/PCSX2_suite.sln +++ b/PCSX2_suite.sln @@ -60,6 +60,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "common", "common\common.vcx EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "glad", "3rdparty\glad\glad.vcxproj", "{C0293B32-5ACF-40F0-AA6C-E6DA6F3BF33A}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cubeb", "3rdparty\cubeb\cubeb.vcxproj", "{BF74C473-DC04-44B3-92E8-4145F4E77342}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug AVX2|Win32 = Debug AVX2|Win32 @@ -604,6 +606,30 @@ Global {C0293B32-5ACF-40F0-AA6C-E6DA6F3BF33A}.Release|Win32.Build.0 = Release|Win32 {C0293B32-5ACF-40F0-AA6C-E6DA6F3BF33A}.Release|x64.ActiveCfg = Release|x64 {C0293B32-5ACF-40F0-AA6C-E6DA6F3BF33A}.Release|x64.Build.0 = Release|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug AVX2|Win32.ActiveCfg = Debug|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug AVX2|Win32.Build.0 = Debug|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug AVX2|x64.ActiveCfg = Debug|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug AVX2|x64.Build.0 = Debug|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug|Win32.ActiveCfg = Debug|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug|Win32.Build.0 = Debug|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug|x64.ActiveCfg = Debug|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Debug|x64.Build.0 = Debug|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel AVX2|Win32.ActiveCfg = Devel|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel AVX2|Win32.Build.0 = Devel|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel AVX2|x64.ActiveCfg = Devel|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel AVX2|x64.Build.0 = Devel|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel|Win32.ActiveCfg = Devel|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel|Win32.Build.0 = Devel|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel|x64.ActiveCfg = Devel|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Devel|x64.Build.0 = Devel|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release AVX2|Win32.ActiveCfg = Release|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release AVX2|Win32.Build.0 = Release|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release AVX2|x64.ActiveCfg = Release|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release AVX2|x64.Build.0 = Release|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release|Win32.ActiveCfg = Release|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release|Win32.Build.0 = Release|Win32 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release|x64.ActiveCfg = Release|x64 + {BF74C473-DC04-44B3-92E8-4145F4E77342}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -629,6 +655,7 @@ Global {A0D2B3AD-1F72-4EE3-8B5C-F2C358DA35F0} = {78EBE642-7A4D-4EA7-86BE-5639C6646C38} {ED2F21FD-0A36-4A8F-9B90-E7D92A2ACB63} = {78EBE642-7A4D-4EA7-86BE-5639C6646C38} {C0293B32-5ACF-40F0-AA6C-E6DA6F3BF33A} = {78EBE642-7A4D-4EA7-86BE-5639C6646C38} + {BF74C473-DC04-44B3-92E8-4145F4E77342} = {78EBE642-7A4D-4EA7-86BE-5639C6646C38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0BC474EA-3628-45D3-9DBC-E22D0B7E0F77} diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index f3b57b0c75..2d52c4bcb0 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -304,6 +304,12 @@ if(TARGET PkgConfig::PORTAUDIO) list(APPEND pcsx2SPU2Sources SPU2/SndOut_Portaudio.cpp) endif() +if(CUBEB_API) + list(APPEND pcsx2SPU2Sources SPU2/SndOut_Cubeb.cpp) + target_compile_definitions(PCSX2_FLAGS INTERFACE "SPU2X_CUBEB") + target_link_libraries(PCSX2_FLAGS INTERFACE cubeb) +endif() + # SPU2 headers set(pcsx2SPU2Headers SPU2/Config.h diff --git a/pcsx2/SPU2/SndOut.cpp b/pcsx2/SPU2/SndOut.cpp index b6f291d3d5..e92615d68b 100644 --- a/pcsx2/SPU2/SndOut.cpp +++ b/pcsx2/SPU2/SndOut.cpp @@ -88,6 +88,9 @@ SndOutModule* mods[] = #if defined(SPU2X_PORTAUDIO) PortaudioOut, #endif +#if defined(SPU2X_CUBEB) + CubebOut, +#endif #if defined(__linux__) || defined(__APPLE__) SDLOut, #endif @@ -138,6 +141,7 @@ bool SndBuffer::CheckUnderrunStatus(int& nSamples, int& quietSampleCount) if (data < toFill) { quietSampleCount = nSamples; + nSamples = 0; return false; } @@ -148,8 +152,8 @@ bool SndBuffer::CheckUnderrunStatus(int& nSamples, int& quietSampleCount) } else if (data < nSamples) { + quietSampleCount = nSamples - data; nSamples = data; - quietSampleCount = SndOutPacketSize - data; m_underrun_freeze = true; if (SynchMode == 0) // TimeStrech on @@ -236,10 +240,8 @@ void SndBuffer::_ReadSamples_Safe(StereoOut32* bData, int nSamples) // the sample output is determined by the SndOutVolumeShift, which is the number of bits // to shift right to get a 16 bit result. template -void SndBuffer::ReadSamples(T* bData) +void SndBuffer::ReadSamples(T* bData, int nSamples) { - int nSamples = SndOutPacketSize; - // Problem: // If the SPU2 gets even the least bit out of sync with the SndOut device, // the readpos of the circular buffer will overtake the writepos, @@ -293,29 +295,30 @@ void SndBuffer::ReadSamples(T* bData) // If quietSamples != 0 it means we have an underrun... // Let's just dull out some silence, because that's usually the least // painful way of dealing with underruns: - std::fill_n(bData, quietSamples, T{}); + if (quietSamples > 0) + std::memset(bData + nSamples, 0, sizeof(T) * quietSamples); } -template void SndBuffer::ReadSamples(StereoOut16*); -template void SndBuffer::ReadSamples(StereoOut32*); +template void SndBuffer::ReadSamples(StereoOut16*, int); +template void SndBuffer::ReadSamples(StereoOut32*, int); //template void SndBuffer::ReadSamples(StereoOutFloat*); -template void SndBuffer::ReadSamples(Stereo21Out16*); -template void SndBuffer::ReadSamples(Stereo40Out16*); -template void SndBuffer::ReadSamples(Stereo41Out16*); -template void SndBuffer::ReadSamples(Stereo51Out16*); -template void SndBuffer::ReadSamples(Stereo51Out16Dpl*); -template void SndBuffer::ReadSamples(Stereo51Out16DplII*); -template void SndBuffer::ReadSamples(Stereo71Out16*); +template void SndBuffer::ReadSamples(Stereo21Out16*, int); +template void SndBuffer::ReadSamples(Stereo40Out16*, int); +template void SndBuffer::ReadSamples(Stereo41Out16*, int); +template void SndBuffer::ReadSamples(Stereo51Out16*, int); +template void SndBuffer::ReadSamples(Stereo51Out16Dpl*, int); +template void SndBuffer::ReadSamples(Stereo51Out16DplII*, int); +template void SndBuffer::ReadSamples(Stereo71Out16*, int); -template void SndBuffer::ReadSamples(Stereo20Out32*); -template void SndBuffer::ReadSamples(Stereo21Out32*); -template void SndBuffer::ReadSamples(Stereo40Out32*); -template void SndBuffer::ReadSamples(Stereo41Out32*); -template void SndBuffer::ReadSamples(Stereo51Out32*); -template void SndBuffer::ReadSamples(Stereo51Out32Dpl*); -template void SndBuffer::ReadSamples(Stereo51Out32DplII*); -template void SndBuffer::ReadSamples(Stereo71Out32*); +template void SndBuffer::ReadSamples(Stereo20Out32*, int); +template void SndBuffer::ReadSamples(Stereo21Out32*, int); +template void SndBuffer::ReadSamples(Stereo40Out32*, int); +template void SndBuffer::ReadSamples(Stereo41Out32*, int); +template void SndBuffer::ReadSamples(Stereo51Out32*, int); +template void SndBuffer::ReadSamples(Stereo51Out32Dpl*, int); +template void SndBuffer::ReadSamples(Stereo51Out32DplII*, int); +template void SndBuffer::ReadSamples(Stereo71Out32*, int); void SndBuffer::_WriteSamples(StereoOut32* bData, int nSamples) { diff --git a/pcsx2/SPU2/SndOut.h b/pcsx2/SPU2/SndOut.h index eb083010d6..66066b0e14 100644 --- a/pcsx2/SPU2/SndOut.h +++ b/pcsx2/SPU2/SndOut.h @@ -625,7 +625,7 @@ public: // the sample output is determined by the SndOutVolumeShift, which is the number of bits // to shift right to get a 16 bit result. template - static void ReadSamples(T* bData); + static void ReadSamples(T* bData, int nSamples = SndOutPacketSize); }; class SndOutModule @@ -670,6 +670,9 @@ extern SndOutModule* XAudio2Out; #if defined(SPU2X_PORTAUDIO) extern SndOutModule* PortaudioOut; #endif +#if defined(SPU2X_CUBEB) +extern SndOutModule* CubebOut; +#endif extern SndOutModule* const SDLOut; extern SndOutModule* mods[]; diff --git a/pcsx2/SPU2/SndOut_Cubeb.cpp b/pcsx2/SPU2/SndOut_Cubeb.cpp new file mode 100644 index 0000000000..7069976402 --- /dev/null +++ b/pcsx2/SPU2/SndOut_Cubeb.cpp @@ -0,0 +1,370 @@ +/* PCSX2 - PS2 Emulator for PCs + * Copyright (C) 2002-2021 PCSX2 Dev Team + * + * PCSX2 is free software: you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCSX2. + * If not, see . + */ + +#include "PrecompiledHeader.h" + +#include "common/Console.h" +#include "common/StringUtil.h" +#include "common/RedtapeWindows.h" +#include "cubeb/cubeb.h" + +#include "Global.h" +#include "SndOut.h" + +extern bool CfgReadBool(const wchar_t* Section, const wchar_t* Name, bool Default); +extern int CfgReadInt(const wchar_t* Section, const wchar_t* Name, int Default); +extern void CfgReadStr(const wchar_t* Section, const wchar_t* Name, wxString& Data, const wchar_t* Default); + +class Cubeb : public SndOutModule +{ +private: + static constexpr int MINIMUM_LATENCY_MS = 20; + static constexpr int MAXIMUM_LATENCY_MS = 200; + + ////////////////////////////////////////////////////////////////////////////////////////// + // Stuff necessary for speaker expansion + class SampleReader + { + public: + virtual void ReadSamples(void* outputBuffer, long frames) = 0; + }; + + template + class ConvertedSampleReader final : public SampleReader + { + u64* const written; + + public: + ConvertedSampleReader() = delete; + + ConvertedSampleReader(u64* pWritten) + : written(pWritten) + { + } + + void ReadSamples(void* outputBuffer, long frames) override + { + T* p1 = static_cast(outputBuffer); + + while (frames > 0) + { + const long frames_to_read = std::min(frames, SndOutPacketSize); + SndBuffer::ReadSamples(p1, frames_to_read); + p1 += frames_to_read; + frames -= frames_to_read; + } + + (*written) += frames; + } + }; + + void DestroyContextAndStream() + { + if (stream) + { + cubeb_stream_stop(stream); + cubeb_stream_destroy(stream); + stream = nullptr; + } + + if (m_context) + { + cubeb_destroy(m_context); + m_context = nullptr; + } + + ActualReader.reset(); + +#ifdef _WIN32 + if (m_COMInitializedByUs) + { + CoUninitialize(); + m_COMInitializedByUs = false; + } +#endif + } + + static void LogCallback(const char* fmt, ...) + { + FastFormatAscii msg; + std::va_list ap; + va_start(ap, fmt); + msg.WriteV(fmt, ap); + va_end(ap); + Console.WriteLn("(Cubeb): %s", msg.c_str()); + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // Configuration Vars + bool m_COMInitializedByUs = false; + bool m_SuggestedLatencyMinimal = false; + int m_SuggestedLatencyMS = 20; + std::string m_Backend; + + ////////////////////////////////////////////////////////////////////////////////////////// + // Instance vars + u64 writtenSoFar = 0; + u64 writtenLastTime = 0; + u64 positionLastTime = 0; + + u32 channels = 0; + cubeb* m_context = nullptr; + cubeb_stream* stream = nullptr; + std::unique_ptr ActualReader; + bool m_paused = false; + + +public: + Cubeb() = default; + + ~Cubeb() + { + DestroyContextAndStream(); + } + + s32 Init() override + { + ReadSettings(); + + // TODO(Stenzek): Migrate the errors to Host::ReportErrorAsync() once more Qt stuff is merged. + +#ifdef _WIN32 + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + m_COMInitializedByUs = SUCCEEDED(hr); + if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) + { + Console.Error("Failed to initialize COM"); + return -1; + } +#endif + +#ifdef PCSX2_DEVBUILD + cubeb_set_log_callback(CUBEB_LOG_NORMAL, LogCallback); +#endif + + int rv = cubeb_init(&m_context, "PCSX2", m_Backend.empty() ? nullptr : m_Backend.c_str()); + if (rv != CUBEB_OK) + { + Console.Error("Could not initialize cubeb context: %d", rv); + return -1; + } + + switch (numSpeakers) // speakers = (numSpeakers + 1) *2; ? + { + case 0: + channels = 2; + break; // Stereo + case 1: + channels = 4; + break; // Quadrafonic + case 2: + channels = 6; + break; // Surround 5.1 + case 3: + channels = 8; + break; // Surround 7.1 + default: + channels = 2; + break; + } + + cubeb_channel_layout layout = CUBEB_LAYOUT_UNDEFINED; + switch (channels) + { + case 2: + Console.WriteLn("(Cubeb) Using normal 2 speaker stereo output."); + ActualReader = std::make_unique>(&writtenSoFar); + break; + + case 3: + Console.WriteLn("(Cubeb) 2.1 speaker expansion enabled."); + ActualReader = std::make_unique>(&writtenSoFar); + layout = CUBEB_LAYOUT_STEREO_LFE; + break; + + case 4: + Console.WriteLn("(Cubeb) 4 speaker expansion enabled [quadraphenia]"); + ActualReader = std::make_unique>(&writtenSoFar); + layout = CUBEB_LAYOUT_QUAD; + break; + + case 5: + Console.WriteLn("(Cubeb) 4.1 speaker expansion enabled."); + ActualReader = std::make_unique>(&writtenSoFar); + layout = CUBEB_LAYOUT_QUAD_LFE; + break; + + case 6: + case 7: + switch (dplLevel) + { + case 0: + Console.WriteLn("(Cubeb) 5.1 speaker expansion enabled."); + ActualReader = std::make_unique>(&writtenSoFar); //"normal" stereo upmix + break; + case 1: + Console.WriteLn("(Cubeb) 5.1 speaker expansion with basic ProLogic dematrixing enabled."); + ActualReader = std::make_unique>(&writtenSoFar); // basic Dpl decoder without rear stereo balancing + break; + case 2: + Console.WriteLn("(Cubeb) 5.1 speaker expansion with experimental ProLogicII dematrixing enabled."); + ActualReader = std::make_unique>(&writtenSoFar); //gigas PLII + break; + } + channels = 6; // we do not support 7.0 or 6.2 configurations, downgrade to 5.1 + layout = CUBEB_LAYOUT_3F2_LFE; + break; + + default: // anything 8 or more gets the 7.1 treatment! + Console.WriteLn("(Cubeb) 7.1 speaker expansion enabled."); + ActualReader = std::make_unique>(&writtenSoFar); + channels = 8; // we do not support 7.2 or more, downgrade to 7.1 + layout = CUBEB_LAYOUT_3F4_LFE; + break; + } + + cubeb_stream_params params = {}; + params.format = CUBEB_SAMPLE_S16LE; + params.rate = SampleRate; + params.channels = channels; + params.layout = layout; + params.prefs = CUBEB_STREAM_PREF_NONE; + + const u32 requested_latency_frames = static_cast((m_SuggestedLatencyMS * static_cast(SampleRate)) / 1000u); + u32 latency_frames = 0; + rv = cubeb_get_min_latency(m_context, ¶ms, &latency_frames); + if (rv == CUBEB_ERROR_NOT_SUPPORTED) + { + Console.WriteLn("(Cubeb) Cubeb backend does not support latency queries, using latency of %d ms (%u frames).", + m_SuggestedLatencyMS, requested_latency_frames); + latency_frames = requested_latency_frames; + } + else + { + if (rv != CUBEB_OK) + { + Console.Error("Could not get minimum latency: %d", rv); + DestroyContextAndStream(); + return -1; + } + + const float minimum_latency_ms = static_cast(latency_frames * 1000u) / static_cast(SampleRate); + Console.WriteLn("(Cubeb) Minimum latency: %.2f ms (%u audio frames)", minimum_latency_ms, latency_frames); + if (!m_SuggestedLatencyMinimal) + { + if (latency_frames > requested_latency_frames) + { + Console.Warning("(Cubeb) Minimum latency is above requested latency: %u vs %u, adjusting to compensate.", + latency_frames, requested_latency_frames); + } + else + { + latency_frames = requested_latency_frames; + } + } + } + + char stream_name[32]; + std::snprintf(stream_name, sizeof(stream_name), "%p", this); + + rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, nullptr, ¶ms, + latency_frames, &Cubeb::DataCallback, &Cubeb::StateCallback, this); + if (rv != CUBEB_OK) + { + Console.Error("Could not create stream: %d", rv); + DestroyContextAndStream(); + return -1; + } + + rv = cubeb_stream_start(stream); + if (rv != CUBEB_OK) + { + Console.Error("Could not start stream: %d", rv); + DestroyContextAndStream(); + return -1; + } + + return 0; + } + + void Close() override + { + DestroyContextAndStream(); + } + + static void StateCallback(cubeb_stream* stream, void* user_ptr, cubeb_state state) + { + } + + static long DataCallback(cubeb_stream* stm, void* user_ptr, const void* input_buffer, void* output_buffer, long nframes) + { + static_cast(user_ptr)->ActualReader->ReadSamples(output_buffer, nframes); + return nframes; + } + + + void Configure(uptr parent) override + { + } + + s32 Test() const override + { + return 0; + } + + int GetEmptySampleCount() override + { + u64 pos; + if (cubeb_stream_get_position(stream, &pos) != CUBEB_OK) + pos = 0; + + const int playedSinceLastTime = (writtenSoFar - writtenLastTime) + (pos - positionLastTime); + writtenLastTime = writtenSoFar; + positionLastTime = pos; + return playedSinceLastTime; + } + + const wchar_t* GetIdent() const override + { + return L"cubeb"; + } + + const wchar_t* GetLongName() const override + { + return L"Cubeb (Cross-platform)"; + } + + void ReadSettings() override + { + m_SuggestedLatencyMinimal = CfgReadBool(L"Cubeb", L"MinimalSuggestedLatency", false); + m_SuggestedLatencyMS = std::clamp(CfgReadInt(L"Cubeb", L"ManualSuggestedLatencyMS", MINIMUM_LATENCY_MS), MINIMUM_LATENCY_MS, MAXIMUM_LATENCY_MS); + + // TODO: Once the config stuff gets merged, drop the wxString here. + wxString backend; + CfgReadStr(L"Cubeb", L"BackendName", backend, L""); + m_Backend = StringUtil::wxStringToUTF8String(backend); + } + + void SetApiSettings(wxString api) override + { + } + + void WriteSettings() const override + { + } +}; + +static Cubeb s_Cubeb; +SndOutModule* CubebOut = &s_Cubeb; diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj index 93beddd4d6..d23f26c5fe 100644 --- a/pcsx2/pcsx2.vcxproj +++ b/pcsx2/pcsx2.vcxproj @@ -41,13 +41,14 @@ $(SolutionDir)3rdparty\zlib;%(AdditionalIncludeDirectories) $(SolutionDir)3rdparty\libpng;%(AdditionalIncludeDirectories) $(SolutionDir)3rdparty\glad\include;%(AdditionalIncludeDirectories) + $(SolutionDir)3rdparty\cubeb\cubeb\include;$(SolutionDir)3rdparty\cubeb\include;%(AdditionalIncludeDirectories) Async Use PrecompiledHeader.h PrecompiledHeader.h;%(ForcedIncludeFiles) NoExtensions /Zc:externConstexpr %(AdditionalOptions) - WIN32_LEAN_AND_MEAN;LZMA_API_STATIC;BUILD_DX=1;SPU2X_PORTAUDIO;DIRECTINPUT_VERSION=0x0800;%(PreprocessorDefinitions) + WIN32_LEAN_AND_MEAN;LZMA_API_STATIC;BUILD_DX=1;SPU2X_CUBEB;SPU2X_PORTAUDIO;DIRECTINPUT_VERSION=0x0800;%(PreprocessorDefinitions) PCSX2_DEBUG;PCSX2_DEVBUILD;_SECURE_SCL_=1;%(PreprocessorDefinitions) PCSX2_DEVEL;PCSX2_DEVBUILD;NDEBUG;_SECURE_SCL_=1;%(PreprocessorDefinitions) NDEBUG;_SECURE_SCL_=0;%(PreprocessorDefinitions) @@ -357,6 +358,7 @@ + @@ -1157,6 +1159,9 @@ {ed2f21fd-0a36-4a8f-9b90-e7d92a2acb63} + + {bf74c473-dc04-44b3-92e8-4145f4e77342} + {4639972e-424e-4e13-8b07-ca403c481346} diff --git a/pcsx2/pcsx2.vcxproj.filters b/pcsx2/pcsx2.vcxproj.filters index de9b7bd909..96f31e9a9c 100644 --- a/pcsx2/pcsx2.vcxproj.filters +++ b/pcsx2/pcsx2.vcxproj.filters @@ -1647,6 +1647,9 @@ System + + System\Ps2\SPU2 +