From dbe5384724ce97e82c9a0a0185c0818d9ca6d986 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Mon, 25 Apr 2022 19:54:09 +0100 Subject: [PATCH] [nx] add audio seems to sound very good! not perfect yet because not handling too many samples generated and need a better way to handle underflow. see #70 --- src/frontend/backend/nx/CMakeLists.txt | 2 + src/frontend/backend/nx/audio/audio.cpp | 267 ++++++++++++++++++++++++ src/frontend/backend/nx/audio/audio.hpp | 10 + src/frontend/backend/nx/backend_nx.cpp | 43 ++-- 4 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 src/frontend/backend/nx/audio/audio.cpp create mode 100644 src/frontend/backend/nx/audio/audio.hpp diff --git a/src/frontend/backend/nx/CMakeLists.txt b/src/frontend/backend/nx/CMakeLists.txt index 012bb19..656c591 100644 --- a/src/frontend/backend/nx/CMakeLists.txt +++ b/src/frontend/backend/nx/CMakeLists.txt @@ -4,6 +4,8 @@ add_library(backend backend_nx.cpp ftpd_imgui/imgui_deko3d.cpp ftpd_imgui/imgui_nx.cpp + + audio/audio.cpp ) target_link_libraries(backend PRIVATE imgui) diff --git a/src/frontend/backend/nx/audio/audio.cpp b/src/frontend/backend/nx/audio/audio.cpp new file mode 100644 index 0000000..b04d4a5 --- /dev/null +++ b/src/frontend/backend/nx/audio/audio.cpp @@ -0,0 +1,267 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only + +// this code is mostly from an old audio player i made +// for the switch, was never released however + +// the "voice" can be created with any sample rate +// and the audio hw handles the resampling to the device output +// this means it can take the 4 sample rates [32768, 65536, 131072, 262144] +// supported by gba, however sampling at anything higher than 65k is taxing. + +// theres no smart handling if too many samples are created. +// if this does happen, samples are dropped. + +// theres very basic time stretching which stretches the +// last sample if there not enough samples, this actually sounds +// very good! + +#include "../../../system.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// custom allocator for std::vector that respects pool alignment. +template +class PoolAllocator { +public: + using value_type = Type; // used by std::vector + +public: + constexpr PoolAllocator() = default; + constexpr ~PoolAllocator() = default; + + [[nodiscard]] + constexpr auto allocate(const std::size_t n) -> Type* { + return new(align) Type[n]; + } + + constexpr auto deallocate(Type* p, const std::size_t n) noexcept -> void { + delete[] (align, p); + } + +private: + static constexpr std::align_val_t align{0x1000}; +}; + +constexpr AudioRendererConfig cfg = { + .output_rate = AudioRendererOutputRate_48kHz, + .num_voices = 2, + .num_effects = 0, + .num_sinks = 1, + .num_mix_objs = 1, + .num_mix_buffers = 2, +}; + +AudioDriver driver; + +constexpr int voice_id = 0; +constexpr int channels = 2; +constexpr int samples = 4096 * 2; +constexpr int frequency = 65536; +constexpr uint8_t sink_channels[channels]{ 0, 1 }; + +int sink_id; +int mem_pool_id; + +std::vector> mem_pool; +std::size_t spec_size; + +// this is what we write the samples into +std::vector temp_buf; +std::size_t temp_buffer_index; + +// this is what we copy the temp_buf into per audio frame +std::vector wave_buffers; +std::size_t wave_buffer_index; + +std::jthread thread; +std::mutex mutex; + +auto audio_callback(void* user, int16_t left, int16_t right) -> void +{ + std::scoped_lock lock{mutex}; + + if (temp_buffer_index >= temp_buf.size()) + { + return; + } + + temp_buf[temp_buffer_index++] = left; + temp_buf[temp_buffer_index++] = right; +} + +auto audio_thread(std::stop_token token) -> void +{ + for (;;) + { + if (token.stop_requested()) + { + printf("[INFO] stop token requested in loop!\n"); + return; + } + + auto& buffer = wave_buffers[wave_buffer_index]; + if (buffer.state == AudioDriverWaveBufState_Free || buffer.state == AudioDriverWaveBufState_Done) + { + // apparently mem_pool data shouldn't be used directly (opus example). + // so we use a temp buffer then copy in the buffer. + auto data = mem_pool.data() + (wave_buffer_index * spec_size); + { + std::scoped_lock lock{mutex}; + auto data16 = reinterpret_cast(data); + std::ranges::copy(temp_buf, data16); + + // stretch last sample + if (temp_buffer_index >= 2 && temp_buffer_index < temp_buf.size()) + { + for (size_t i = temp_buffer_index; i < temp_buf.size(); i++) + { + data16[i] = temp_buf[temp_buffer_index - 2]; + } + } + + temp_buffer_index = 0; + } + armDCacheFlush(data, spec_size); + + if (!audrvVoiceAddWaveBuf(&driver, voice_id, &buffer)) + { + printf("[ERROR] failed to add wave buffer to voice!\n"); + } + + // resume voice is it was idle or stopped playing. + if (!audrvVoiceIsPlaying(&driver, voice_id)) + { + audrvVoiceStart(&driver, voice_id); + } + + // advance the buffer index + wave_buffer_index = (wave_buffer_index + 1) % wave_buffers.size(); + } + + if (auto r = audrvUpdate(&driver); R_FAILED(r)) + { + printf("[ERROR] failed to update audio driver in loop!\n"); + } + + audrenWaitFrame(); + } +} + +} // namespace + +namespace nx::audio { + +auto init() -> bool +{ + wave_buffers.resize(2); + + if (auto r = audrenInitialize(&cfg); R_FAILED(r)) + { + printf("failed to init audren\n"); + return false; + } + + if (auto r = audrvCreate(&driver, &cfg, channels); R_FAILED(r)) + { + printf("failed to create driver\n"); + return false; + } + + sink_id = audrvDeviceSinkAdd(&driver, AUDREN_DEFAULT_DEVICE_NAME, channels, sink_channels); + + if (auto r = audrvUpdate(&driver); R_FAILED(r)) + { + printf("failed to add sink to driver\n"); + return false; + } + + if (auto r = audrenStartAudioRenderer(); R_FAILED(r)) + { + printf("failed to start audio renderer\n"); + return false; + } + + if (!audrvVoiceInit(&driver, voice_id, channels, PcmFormat_Int16, frequency)) + { + printf("failed to init voice\n"); + return false; + } + + audrvVoiceSetDestinationMix(&driver, voice_id, AUDREN_FINAL_MIX_ID); + if (channels == 1) + { + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 0, 0); + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 0, 1); + } + else + { + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 0, 0); + audrvVoiceSetMixFactor(&driver, voice_id, 0.0F, 0, 1); + audrvVoiceSetMixFactor(&driver, voice_id, 0.0F, 1, 0); + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 1, 1); + } + + spec_size = sizeof(std::int16_t) * channels * samples; + const auto mem_pool_size = ((spec_size * wave_buffers.size()) + (AUDREN_MEMPOOL_ALIGNMENT - 1)) &~ (AUDREN_MEMPOOL_ALIGNMENT - 1); + mem_pool.resize(mem_pool_size); + // LOG("unaliged size 0x%lX aligned size: 0x%lX vector size: 0x%lX\n", spec_size * wave_buffers.size(), ((spec_size * wave_buffers.size()) + (AUDREN_MEMPOOL_ALIGNMENT - 1)) &~ (AUDREN_MEMPOOL_ALIGNMENT - 1), mem_pool.size()); + + for (std::size_t i = 0; i < wave_buffers.size(); ++i) { + wave_buffers[i].data_adpcm = mem_pool.data(); + wave_buffers[i].size = mem_pool.size(); + wave_buffers[i].start_sample_offset = i * samples; + wave_buffers[i].end_sample_offset = wave_buffers[i].start_sample_offset + samples; + } + + armDCacheFlush(mem_pool.data(), mem_pool.size()); + + mem_pool_id = audrvMemPoolAdd(&driver, mem_pool.data(), mem_pool.size()); + if (!audrvMemPoolAttach(&driver, mem_pool_id)) + { + printf("[ERROR] failed to attach mem pool!\n"); + return false; + } + + wave_buffer_index = 0; + temp_buf.resize(spec_size / 2); // this is s16 + thread = std::jthread(audio_thread); + sys::System::gameboy_advance.set_audio_callback(audio_callback); + + return true; +} + +auto quit() -> void +{ + // this may take a while to join! + // its probably possible to wait on an audio event + // which if thats the case, then i can manually wake up + // the thread whenever i want. + thread.request_stop(); + thread.join(); + printf("[INFO] joined audio loop thread\n"); + + if (auto r = audrenStopAudioRenderer(); R_FAILED(r)) + { + printf("[ERROR] failed to stop audren!\n"); + } + + audrvVoiceDrop(&driver, voice_id); + audrvClose(&driver); + audrenExit(); + + mem_pool.clear(); +} + +} // namespace nx::audio diff --git a/src/frontend/backend/nx/audio/audio.hpp b/src/frontend/backend/nx/audio/audio.hpp new file mode 100644 index 0000000..e661f29 --- /dev/null +++ b/src/frontend/backend/nx/audio/audio.hpp @@ -0,0 +1,10 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only +#pragma once + +namespace nx::audio { + +auto init() -> bool; +auto quit() -> void; + +} // namespace nx::audio diff --git a/src/frontend/backend/nx/backend_nx.cpp b/src/frontend/backend/nx/backend_nx.cpp index 705cb65..2c3609f 100644 --- a/src/frontend/backend/nx/backend_nx.cpp +++ b/src/frontend/backend/nx/backend_nx.cpp @@ -5,6 +5,7 @@ #include "ftpd_imgui/imgui_nx.h" #include "../backend.hpp" #include "../../system.hpp" +#include "audio/audio.hpp" #include #include #include @@ -105,7 +106,7 @@ AppletHookCookie appletHookCookie; auto applet_show_error_message(const char* message, const char* long_message) { ErrorApplicationConfig cfg; - errorApplicationCreate(&cfg, "Unsupported Launch!", "Please launch as application!"); + errorApplicationCreate(&cfg, message, long_message); errorApplicationShow(&cfg); } @@ -431,7 +432,7 @@ auto Texture::quit() -> void if (!imgui::nx::init()) { - applet_show_error_message("Unsupported Launch!", "Please launch as application!"); + applet_show_error_message("Failed to init imgui!", ""); return false; } @@ -454,11 +455,19 @@ auto Texture::quit() -> void padConfigureInput(1, HidNpadStyleSet_NpadStandard); padInitializeDefault(&pad); + if (!nx::audio::init()) + { + applet_show_error_message("failed to open audio!", ""); + return false; + } + return true; } auto quit() -> void { + nx::audio::quit(); + imgui::nx::exit(); // wait for queue to be idle @@ -489,24 +498,30 @@ auto poll_events() -> void } padUpdate(&pad); - const auto down = padGetButtons(&pad); - - System::emu_set_button(gba::A, !!(down & HidNpadButton_A)); - System::emu_set_button(gba::B, !!(down & HidNpadButton_B)); - System::emu_set_button(gba::L, !!(down & HidNpadButton_L)); - System::emu_set_button(gba::R, !!(down & HidNpadButton_R)); - System::emu_set_button(gba::START, !!(down & HidNpadButton_Plus)); - System::emu_set_button(gba::SELECT, !!(down & HidNpadButton_Minus)); - System::emu_set_button(gba::UP, !!(down & HidNpadButton_AnyUp)); - System::emu_set_button(gba::DOWN, !!(down & HidNpadButton_AnyDown)); - System::emu_set_button(gba::LEFT, !!(down & HidNpadButton_AnyLeft)); - System::emu_set_button(gba::RIGHT, !!(down & HidNpadButton_AnyRight)); + const auto buttons = padGetButtons(&pad); + const auto down = padGetButtonsDown(&pad); + + System::emu_set_button(gba::A, !!(buttons & HidNpadButton_A)); + System::emu_set_button(gba::B, !!(buttons & HidNpadButton_B)); + System::emu_set_button(gba::L, !!(buttons & HidNpadButton_L)); + System::emu_set_button(gba::R, !!(buttons & HidNpadButton_R)); + System::emu_set_button(gba::START, !!(buttons & HidNpadButton_Plus)); + System::emu_set_button(gba::SELECT, !!(buttons & HidNpadButton_Minus)); + System::emu_set_button(gba::UP, !!(buttons & HidNpadButton_AnyUp)); + System::emu_set_button(gba::DOWN, !!(buttons & HidNpadButton_AnyDown)); + System::emu_set_button(gba::LEFT, !!(buttons & HidNpadButton_AnyLeft)); + System::emu_set_button(gba::RIGHT, !!(buttons & HidNpadButton_AnyRight)); if (!!(down & HidNpadButton_ZR)) { System::running = false; } + if (!!(down & HidNpadButton_ZL)) + { + System::loadstate(System::rom_path); + } + // this only update inputs and screen size // so it should be called in poll events imgui::nx::newFrame(&pad);