diff --git a/.gitmodules b/.gitmodules index 7d234ac6..77a6bc6a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -55,6 +55,9 @@ [submodule "third_party/libuv"] path = third_party/libuv url = https://github.com/libuv/libuv +[submodule "third_party/miniaudio"] + path = third_party/miniaudio + url = https://github.com/mackron/miniaudio [submodule "third_party/teakra"] path = third_party/teakra url = https://github.com/wwylele/teakra diff --git a/CMakeLists.txt b/CMakeLists.txt index 43391513..4dbe438f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ include_directories(third_party/xxhash/include) include_directories(third_party/httplib) include_directories(third_party/stb) include_directories(third_party/opengl) +include_directories(third_party/miniaudio) include_directories(third_party/mio/single_include) add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it @@ -155,7 +156,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp - src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp + src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp @@ -192,7 +193,9 @@ set(FS_SOURCE_FILES src/core/fs/archive_self_ncch.cpp src/core/fs/archive_save_d set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selector.cpp src/core/applets/software_keyboard.cpp src/core/applets/applet_manager.cpp src/core/applets/error_applet.cpp ) -set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp src/core/audio/teakra_core.cpp) +set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp src/core/audio/teakra_core.cpp + src/core/audio/miniaudio_device.cpp +) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files @@ -250,6 +253,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp + include/audio/miniaudio_device.hpp include/ring_buffer.hpp ) cmrc_add_resource_library( diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp index ec73536e..1a556f28 100644 --- a/include/audio/dsp_core.hpp +++ b/include/audio/dsp_core.hpp @@ -9,6 +9,7 @@ #include "helpers.hpp" #include "logger.hpp" #include "scheduler.hpp" +#include "ring_buffer.hpp" // The DSP core must have access to the DSP service to be able to trigger interrupts properly class DSPService; @@ -23,16 +24,22 @@ namespace Audio { static constexpr u64 lleSlice = 16384; class DSPCore { + using Samples = Common::RingBuffer; + protected: Memory& mem; Scheduler& scheduler; DSPService& dspService; + Samples sampleBuffer; + bool audioEnabled = false; + MAKE_LOG_FUNCTION(log, dspLogger) public: enum class Type { Null, Teakra }; - DSPCore(Memory& mem, Scheduler& scheduler, DSPService& dspService) : mem(mem), scheduler(scheduler), dspService(dspService) {} + DSPCore(Memory& mem, Scheduler& scheduler, DSPService& dspService) + : mem(mem), scheduler(scheduler), dspService(dspService) {} virtual ~DSPCore() {} virtual void reset() = 0; @@ -50,6 +57,9 @@ namespace Audio { static Audio::DSPCore::Type typeFromString(std::string inString); static const char* typeToString(Audio::DSPCore::Type type); + + Samples& getSamples() { return sampleBuffer; } + virtual void setAudioEnabled(bool enable) { audioEnabled = enable; } }; std::unique_ptr makeDSPCore(DSPCore::Type type, Memory& mem, Scheduler& scheduler, DSPService& dspService); diff --git a/include/audio/miniaudio_device.hpp b/include/audio/miniaudio_device.hpp new file mode 100644 index 00000000..f4d126d8 --- /dev/null +++ b/include/audio/miniaudio_device.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include + +#include "miniaudio.h" +#include "ring_buffer.hpp" + +class MiniAudioDevice { + using Samples = Common::RingBuffer; + static constexpr ma_uint32 sampleRate = 32768; // 3DS sample rate + static constexpr ma_uint32 channelCount = 2; // Audio output is stereo + + ma_context context; + ma_device_config deviceConfig; + ma_device device; + ma_resampler resampler; + Samples* samples = nullptr; + + bool initialized = false; + bool running = false; + + std::vector audioDevices; + public: + MiniAudioDevice(); + // If safe is on, we create a null audio device + void init(Samples& samples, bool safe = false); + + void start(); + void stop(); +}; \ No newline at end of file diff --git a/include/audio/teakra_core.hpp b/include/audio/teakra_core.hpp index a6311f90..6a011231 100644 --- a/include/audio/teakra_core.hpp +++ b/include/audio/teakra_core.hpp @@ -1,4 +1,6 @@ #pragma once +#include + #include "audio/dsp_core.hpp" #include "memory.hpp" #include "swap.hpp" @@ -10,6 +12,11 @@ namespace Audio { u32 pipeBaseAddr; bool running; // Is the DSP running? bool loaded; // Have we finished loading a binary with LoadComponent? + bool signalledData; + bool signalledSemaphore; + + uint audioFrameIndex = 0; // Index in our audio frame + std::array audioFrame; // Get a pointer to a data memory address u8* getDataPointer(u32 address) { return getDspMemory() + Memory::DSP_DATA_MEMORY_OFFSET + address; } @@ -62,10 +69,6 @@ namespace Audio { std::memcpy(statusAddress + 6, &status.writePointer, sizeof(u16)); } } - - bool signalledData; - bool signalledSemaphore; - // Run 1 slice of DSP instructions void runSlice() { if (running) { @@ -85,6 +88,7 @@ namespace Audio { scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::lleSlice * 2); } + void setAudioEnabled(bool enable) override; u8* getDspMemory() override { return teakra.GetDspMemory().data(); } u16 recvData(u32 regId) override { return teakra.RecvData(regId); } diff --git a/include/config.hpp b/include/config.hpp index e5c10f4b..8c0d2e12 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -22,6 +22,9 @@ struct EmulatorConfig { bool sdWriteProtected = false; bool usePortableBuild = false; + bool audioEnabled = false; + bool vsyncEnabled = true; + bool chargerPlugged = true; // Default to 3% battery to make users suffer int batteryPercentage = 3; diff --git a/include/emulator.hpp b/include/emulator.hpp index 4ed31945..47fbc839 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -8,6 +8,7 @@ #include "PICA/gpu.hpp" #include "audio/dsp_core.hpp" +#include "audio/miniaudio_device.hpp" #include "cheats.hpp" #include "config.hpp" #include "cpu.hpp" @@ -47,6 +48,7 @@ class Emulator { Scheduler scheduler; Crypto::AESEngine aesEngine; + MiniAudioDevice audioDevice; Cheats cheats; // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard @@ -75,6 +77,7 @@ class Emulator { #ifdef PANDA3DS_ENABLE_DISCORD_RPC Discord::RPC discordRpc; #endif + void setAudioEnabled(bool enable); void updateDiscord(); // Keep the handle for the ROM here to reload when necessary and to prevent deleting it diff --git a/include/ring_buffer.hpp b/include/ring_buffer.hpp new file mode 100644 index 00000000..35d7d935 --- /dev/null +++ b/include/ring_buffer.hpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Common { + /// SPSC ring buffer + /// @tparam T Element type + /// @tparam capacity Number of slots in ring buffer + template + class RingBuffer { + /// A "slot" is made of a single `T`. + static constexpr std::size_t slot_size = sizeof(T); + // T must be safely memcpy-able and have a trivial default constructor. + static_assert(std::is_trivial_v); + // Ensure capacity is sensible. + static_assert(capacity < std::numeric_limits::max() / 2); + static_assert((capacity & (capacity - 1)) == 0, "capacity must be a power of two"); + // Ensure lock-free. + static_assert(std::atomic_size_t::is_always_lock_free); + + public: + /// Pushes slots into the ring buffer + /// @param new_slots Pointer to the slots to push + /// @param slot_count Number of slots to push + /// @returns The number of slots actually pushed + std::size_t push(const void* new_slots, std::size_t slot_count) { + const std::size_t write_index = m_write_index.load(); + const std::size_t slots_free = capacity + m_read_index.load() - write_index; + const std::size_t push_count = std::min(slot_count, slots_free); + + const std::size_t pos = write_index % capacity; + const std::size_t first_copy = std::min(capacity - pos, push_count); + const std::size_t second_copy = push_count - first_copy; + + const char* in = static_cast(new_slots); + std::memcpy(m_data.data() + pos, in, first_copy * slot_size); + in += first_copy * slot_size; + std::memcpy(m_data.data(), in, second_copy * slot_size); + + m_write_index.store(write_index + push_count); + + return push_count; + } + + std::size_t push(std::span input) { return push(input.data(), input.size()); } + + /// Pops slots from the ring buffer + /// @param output Where to store the popped slots + /// @param max_slots Maximum number of slots to pop + /// @returns The number of slots actually popped + std::size_t pop(void* output, std::size_t max_slots = ~std::size_t(0)) { + const std::size_t read_index = m_read_index.load(); + const std::size_t slots_filled = m_write_index.load() - read_index; + const std::size_t pop_count = std::min(slots_filled, max_slots); + + const std::size_t pos = read_index % capacity; + const std::size_t first_copy = std::min(capacity - pos, pop_count); + const std::size_t second_copy = pop_count - first_copy; + + char* out = static_cast(output); + std::memcpy(out, m_data.data() + pos, first_copy * slot_size); + out += first_copy * slot_size; + std::memcpy(out, m_data.data(), second_copy * slot_size); + + m_read_index.store(read_index + pop_count); + + return pop_count; + } + + std::vector pop(std::size_t max_slots = ~std::size_t(0)) { + std::vector out(std::min(max_slots, capacity)); + const std::size_t count = Pop(out.data(), out.size()); + out.resize(count); + return out; + } + + /// @returns Number of slots used + [[nodiscard]] std::size_t size() const { return m_write_index.load() - m_read_index.load(); } + + /// @returns Maximum size of ring buffer + [[nodiscard]] constexpr std::size_t Capacity() const { return capacity; } + + private: + // It is important to align the below variables for performance reasons: + // Having them on the same cache-line would result in false-sharing between them. + // TODO: Remove this ifdef whenever clang and GCC support + // std::hardware_destructive_interference_size. +#if defined(__cpp_lib_hardware_interference_size) && !defined(__ANDROID__) + alignas(std::hardware_destructive_interference_size) std::atomic_size_t m_read_index{0}; + alignas(std::hardware_destructive_interference_size) std::atomic_size_t m_write_index{0}; +#else + alignas(128) std::atomic_size_t m_read_index{0}; + alignas(128) std::atomic_size_t m_write_index{0}; +#endif + + std::array m_data; + }; + +} // namespace Common diff --git a/src/config.cpp b/src/config.cpp index 12b112dc..f19ff06d 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -59,6 +59,7 @@ void EmulatorConfig::load() { } shaderJitEnabled = toml::find_or(gpu, "EnableShaderJIT", shaderJitDefault); + vsyncEnabled = toml::find_or(gpu, "EnableVSync", true); } } @@ -69,6 +70,7 @@ void EmulatorConfig::load() { auto dspCoreName = toml::find_or(audio, "DSPEmulation", "Null"); dspType = Audio::DSPCore::typeFromString(dspCoreName); + audioEnabled = toml::find_or(audio, "EnableAudio", false); } } @@ -119,7 +121,9 @@ void EmulatorConfig::save() { data["General"]["UsePortableBuild"] = usePortableBuild; data["GPU"]["EnableShaderJIT"] = shaderJitEnabled; data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType)); + data["GPU"]["EnableVSync"] = vsyncEnabled; data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); + data["Audio"]["EnableAudio"] = audioEnabled; data["Battery"]["ChargerPlugged"] = chargerPlugged; data["Battery"]["BatteryPercentage"] = batteryPercentage; diff --git a/src/core/audio/miniaudio_device.cpp b/src/core/audio/miniaudio_device.cpp new file mode 100644 index 00000000..fa36cb84 --- /dev/null +++ b/src/core/audio/miniaudio_device.cpp @@ -0,0 +1,143 @@ +#include "audio/miniaudio_device.hpp" + +#include "helpers.hpp" + +MiniAudioDevice::MiniAudioDevice() : initialized(false), running(false), samples(nullptr) {} + +void MiniAudioDevice::init(Samples& samples, bool safe) { + this->samples = &samples; + running = false; + + // Probe for device and available backends and initialize audio + ma_backend backends[ma_backend_null + 1]; + uint count = 0; + + if (safe) { + backends[0] = ma_backend_null; + count = 1; + } else { + bool found = false; + for (uint i = 0; i <= ma_backend_null; i++) { + ma_backend backend = ma_backend(i); + if (!ma_is_backend_enabled(backend)) { + continue; + } + + backends[count++] = backend; + + // TODO: Make backend selectable here + found = true; + //count = 1; + //backends[0] = backend; + } + + if (!found) { + initialized = false; + + Helpers::warn("No valid audio backend found\n"); + return; + } + } + + if (ma_context_init(backends, count, nullptr, &context) != MA_SUCCESS) { + initialized = false; + + Helpers::warn("Unable to initialize audio context"); + return; + } + audioDevices.clear(); + + struct UserContext { + MiniAudioDevice* miniAudio; + ma_device_config& config; + bool found = false; + }; + UserContext userContext = {.miniAudio = this, .config = deviceConfig}; + + ma_context_enumerate_devices( + &context, + [](ma_context* pContext, ma_device_type deviceType, const ma_device_info* pInfo, void* pUserData) -> ma_bool32 { + if (deviceType != ma_device_type_playback) { + return true; + } + + UserContext* userContext = reinterpret_cast(pUserData); + userContext->miniAudio->audioDevices.push_back(pInfo->name); + + // TODO: Check if this is the device we want here + userContext->config.playback.pDeviceID = &pInfo->id; + userContext->found = true; + return true; + }, + &userContext + ); + + if (!userContext.found) { + Helpers::warn("MiniAudio: Device not found"); + } + + deviceConfig = ma_device_config_init(ma_device_type_playback); + // The 3DS outputs s16 stereo audio @ 32768 Hz + deviceConfig.playback.format = ma_format_s16; + deviceConfig.playback.channels = channelCount; + deviceConfig.sampleRate = sampleRate; + //deviceConfig.periodSizeInFrames = 64; + //deviceConfig.periods = 16; + deviceConfig.pUserData = this; + deviceConfig.aaudio.usage = ma_aaudio_usage_game; + deviceConfig.wasapi.noAutoConvertSRC = true; + + deviceConfig.dataCallback = [](ma_device* device, void* out, const void* input, ma_uint32 frameCount) { + auto self = reinterpret_cast(device->pUserData); + s16* output = reinterpret_cast(out); + + // Wait until there's enough samples to pop + while (self->samples->size() < frameCount * channelCount) { + // If audio output is disabled from the emulator thread, make sure that this callback will return and not hang + if (!self->running) { + return; + } + } + + self->samples->pop(output, frameCount * channelCount); + }; + + if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { + Helpers::warn("Unable to initialize audio device"); + initialized = false; + return; + } + + initialized = true; +} + +void MiniAudioDevice::start() { + if (!initialized) { + Helpers::warn("MiniAudio device not initialized, won't start"); + return; + } + + // Ignore the call to start if the device is already running + if (!running) { + if (ma_device_start(&device) == MA_SUCCESS) { + running = true; + } else { + Helpers::warn("Failed to start audio device"); + } + } +} + +void MiniAudioDevice::stop() { + if (!initialized) { + Helpers::warn("MiniAudio device not initialized, can't start"); + return; + } + + if (running) { + running = false; + + if (ma_device_stop(&device) != MA_SUCCESS) { + Helpers::warn("Failed to stop audio device"); + } + } +} diff --git a/src/core/audio/teakra_core.cpp b/src/core/audio/teakra_core.cpp index 1f609187..da2e5a5a 100644 --- a/src/core/audio/teakra_core.cpp +++ b/src/core/audio/teakra_core.cpp @@ -1,7 +1,9 @@ #include "audio/teakra_core.hpp" #include +#include #include +#include #include "services/dsp.hpp" @@ -51,10 +53,7 @@ TeakraDSP::TeakraDSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) ahbm.write32 = [&](u32 addr, u32 value) { *(u32*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; }; teakra.SetAHBMCallback(ahbm); - teakra.SetAudioCallback([=](std::array sample) { - //printf("%d %d\n", sample[0], sample[1]); - // NOP for now - }); + teakra.SetAudioCallback([](std::array sample) { /* Do nothing */ }); // Set up event handlers. These handlers forward a hardware interrupt to the DSP service, which is responsible // For triggering the appropriate DSP kernel events @@ -114,6 +113,36 @@ void TeakraDSP::reset() { running = false; loaded = false; signalledData = signalledSemaphore = false; + + audioFrameIndex = 0; +} + +void TeakraDSP::setAudioEnabled(bool enable) { + if (audioEnabled != enable) { + audioEnabled = enable; + + // Set the appropriate audio callback for Teakra + if (audioEnabled) { + teakra.SetAudioCallback([this](std::array sample) { + audioFrame[audioFrameIndex++] = sample[0]; + audioFrame[audioFrameIndex++] = sample[1]; + + // Push our samples at the end of an audio frame + if (audioFrameIndex >= audioFrame.size()) { + audioFrameIndex -= audioFrame.size(); + + // Wait until we've actually got room to do so + while (sampleBuffer.size() + 2 > sampleBuffer.Capacity()) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + + sampleBuffer.push(audioFrame.data(), audioFrame.size()); + } + }); + } else { + teakra.SetAudioCallback([](std::array sample) { /* Do nothing */ }); + } + } } // https://github.com/citra-emu/citra/blob/master/src/audio_core/lle/lle.cpp @@ -313,4 +342,4 @@ void TeakraDSP::unloadComponent() { // Read the value and discard it, completing shutdown teakra.RecvData(2); running = false; -} \ No newline at end of file +} diff --git a/src/emulator.cpp b/src/emulator.cpp index 4481731f..7ddefd6c 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -29,6 +29,9 @@ Emulator::Emulator() dsp = Audio::makeDSPCore(config.dspType, memory, scheduler, dspService); dspService.setDSPCore(dsp.get()); + audioDevice.init(dsp->getSamples()); + setAudioEnabled(config.audioEnabled); + #ifdef PANDA3DS_ENABLE_DISCORD_RPC if (config.discordRpcEnabled) { discordRpc.init(); @@ -102,8 +105,19 @@ void Emulator::step() {} void Emulator::render() {} // Only resume if a ROM is properly loaded -void Emulator::resume() { running = (romType != ROMType::None); } -void Emulator::pause() { running = false; } +void Emulator::resume() { + running = (romType != ROMType::None); + + if (running) { + audioDevice.start(); + } +} + +void Emulator::pause() { + running = false; + audioDevice.stop(); +} + void Emulator::togglePause() { running ? pause() : resume(); } void Emulator::runFrame() { @@ -387,4 +401,16 @@ RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) { dumpRomFSNode(*node, (const char*)&romFS[0], path); return DumpingResult::Success; +} + +void Emulator::setAudioEnabled(bool enable) { + if (!enable) { + audioDevice.stop(); + } else if (enable && romType != ROMType::None && running) { + // Don't start the audio device yet if there's no ROM loaded or the emulator is paused + // Resume and Pause will handle it + audioDevice.start(); + } + + dsp->setAudioEnabled(enable); } \ No newline at end of file diff --git a/src/miniaudio.cpp b/src/miniaudio.cpp new file mode 100644 index 00000000..e42fea68 --- /dev/null +++ b/src/miniaudio.cpp @@ -0,0 +1,7 @@ +// We do not need the ability to be able to encode or decode audio files for the time being +// So we disable said functionality to make the executable smaller +#define MA_NO_DECODING +#define MA_NO_ENCODING +#define MINIAUDIO_IMPLEMENTATION + +#include "miniaudio.h" \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index de70cc18..dff4c171 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -87,7 +87,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) // Make GL context current for this thread, enable VSync GL::Context* glContext = screen.getGLContext(); glContext->MakeCurrent(); - glContext->SetSwapInterval(1); + glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0); emu->initGraphicsContext(glContext); } else if (usingVk) { diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 29b1dcb8..04b582e1 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -49,6 +49,8 @@ FrontendSDL::FrontendSDL() { if (!gladLoadGLLoader(reinterpret_cast(SDL_GL_GetProcAddress))) { Helpers::panic("OpenGL init failed"); } + + SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0); } #ifdef PANDA3DS_ENABLE_VULKAN diff --git a/third_party/miniaudio b/third_party/miniaudio new file mode 160000 index 00000000..4a5b74be --- /dev/null +++ b/third_party/miniaudio @@ -0,0 +1 @@ +Subproject commit 4a5b74bef029b3592c54b6048650ee5f972c1a48