From 21ced6fae7fa3774a152039a9d633935d9dffcf5 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 22 Feb 2024 02:15:49 +0200 Subject: [PATCH] Implement audio output --- .gitmodules | 3 + CMakeLists.txt | 10 ++- include/audio/dsp_core.hpp | 9 ++- include/audio/miniaudio_device.hpp | 28 +++++++ include/emulator.hpp | 2 + include/ring_buffer.hpp | 117 +++++++++++++++++++++++++++ src/core/audio/miniaudio_device.cpp | 119 ++++++++++++++++++++++++++++ src/core/audio/teakra_core.cpp | 9 ++- src/emulator.cpp | 3 +- src/miniaudio.cpp | 7 ++ third_party/miniaudio | 1 + 11 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 include/audio/miniaudio_device.hpp create mode 100644 include/ring_buffer.hpp create mode 100644 src/core/audio/miniaudio_device.cpp create mode 100644 src/miniaudio.cpp create mode 160000 third_party/miniaudio diff --git a/.gitmodules b/.gitmodules index 7d234ac6..aa856a62 100644 --- a/.gitmodules +++ b/.gitmodules @@ -58,3 +58,6 @@ [submodule "third_party/teakra"] path = third_party/teakra url = https://github.com/wwylele/teakra +[submodule "third_party/miniaudio"] + path = third_party/miniaudio + url = https://github.com/mackron/miniaudio diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c0ec255..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 @@ -89,7 +90,6 @@ include_directories(third_party/toml11) include_directories(third_party/glm) add_subdirectory(third_party/cmrc) -add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/third_party/boost") set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost") @@ -150,12 +150,13 @@ if(HOST_X64 OR HOST_ARM64) else() message(FATAL_ERROR "Currently unsupported CPU architecture") endif() +add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) 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 3f1768ff..27e84cbf 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,21 @@ namespace Audio { static constexpr u64 lleSlice = 16384; class DSPCore { + using Samples = Common::RingBuffer; + protected: Memory& mem; Scheduler& scheduler; DSPService& dspService; + Samples sampleBuffer; + 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 void reset() = 0; virtual void runAudioFrame() = 0; @@ -49,6 +55,7 @@ namespace Audio { static Audio::DSPCore::Type typeFromString(std::string inString); static const char* typeToString(Audio::DSPCore::Type type); + Samples& getSamples() { return sampleBuffer; } }; 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..63a54edb --- /dev/null +++ b/include/audio/miniaudio_device.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include + +#include "miniaudio.h" +#include "ring_buffer.hpp" + +class MiniAudioDevice { + using Samples = Common::RingBuffer; + // static constexpr ma_uint32 sampleRateIn = 32768; // 3DS sample rate + // static constexpr ma_uint32 sampleRateOut = 44100; // Output sample rate + + 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(); +}; \ No newline at end of file diff --git a/include/emulator.hpp b/include/emulator.hpp index 207bef46..9420948d 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 diff --git a/include/ring_buffer.hpp b/include/ring_buffer.hpp new file mode 100644 index 00000000..918f23de --- /dev/null +++ b/include/ring_buffer.hpp @@ -0,0 +1,117 @@ +// 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 + +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. +#ifdef __cpp_lib_hardware_interference_size + 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 \ No newline at end of file diff --git a/src/core/audio/miniaudio_device.cpp b/src/core/audio/miniaudio_device.cpp new file mode 100644 index 00000000..8288b6e7 --- /dev/null +++ b/src/core/audio/miniaudio_device.cpp @@ -0,0 +1,119 @@ +#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; + + // Probe for device and available backends and initialize audio + ma_backend backends[ma_backend_null + 1]; + unsigned 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; + printf("Found audio backend: %s\n", ma_get_backend_name(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 = 2; + deviceConfig.sampleRate = 32768; + //deviceConfig.periodSizeInFrames = 64; + //deviceConfig.periods = 2; + 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); + + self->samples->pop(output, frameCount); + }; + + if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { + Helpers::warn("Unable to initialize audio device"); + initialized = false; + return; + } + + initialized = true; + start(); +} + +void MiniAudioDevice::start() { + if (!initialized) { + Helpers::warn("MiniAudio device not initialize, won't start"); + return; + } + + if (ma_device_start(&device) == MA_SUCCESS) { + running = true; + } else { + running = false; + Helpers::warn("Failed to start audio device"); + } +} \ No newline at end of file diff --git a/src/core/audio/teakra_core.cpp b/src/core/audio/teakra_core.cpp index 1f609187..4b8750d8 100644 --- a/src/core/audio/teakra_core.cpp +++ b/src/core/audio/teakra_core.cpp @@ -6,6 +6,10 @@ #include "services/dsp.hpp" using namespace Audio; +static constexpr u32 sampleRate = 32768; +static constexpr u32 duration = 30; +static s16 samples[sampleRate * duration * 2]; +static uint sampleIndex = 0; struct Dsp1 { // All sizes are in bytes unless otherwise specified @@ -51,10 +55,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) { sampleBuffer.push(sample.data(), 2); }); // Set up event handlers. These handlers forward a hardware interrupt to the DSP service, which is responsible // For triggering the appropriate DSP kernel events diff --git a/src/emulator.cpp b/src/emulator.cpp index dbbb0c37..36c9611d 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -25,10 +25,11 @@ Emulator::Emulator() #endif { DSPService& dspService = kernel.getServiceManager().getDSP(); - dsp = Audio::makeDSPCore(config.dspType, memory, scheduler, dspService); dspService.setDSPCore(dsp.get()); + audioDevice.init(dsp->getSamples()); + #ifdef PANDA3DS_ENABLE_DISCORD_RPC if (config.discordRpcEnabled) { discordRpc.init(); 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/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