Skip to content

Commit

Permalink
[nx] add audio
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ITotalJustice committed Apr 25, 2022
1 parent 3b15899 commit dbe5384
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/frontend/backend/nx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
267 changes: 267 additions & 0 deletions src/frontend/backend/nx/audio/audio.cpp
Original file line number Diff line number Diff line change
@@ -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 <switch.h>
#include <switch/result.h>
#include <switch/services/audren.h>
#include <cstddef>
#include <cstdint>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>
#include <ranges>
#include <new>

namespace {

// custom allocator for std::vector that respects pool alignment.
template <typename Type>
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<std::uint8_t, PoolAllocator<std::uint8_t>> mem_pool;
std::size_t spec_size;

// this is what we write the samples into
std::vector<std::int16_t> temp_buf;
std::size_t temp_buffer_index;

// this is what we copy the temp_buf into per audio frame
std::vector<AudioDriverWaveBuf> 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<int16_t*>(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
10 changes: 10 additions & 0 deletions src/frontend/backend/nx/audio/audio.hpp
Original file line number Diff line number Diff line change
@@ -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
43 changes: 29 additions & 14 deletions src/frontend/backend/nx/backend_nx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "ftpd_imgui/imgui_nx.h"
#include "../backend.hpp"
#include "../../system.hpp"
#include "audio/audio.hpp"
#include <cstdint>
#include <switch/applets/error.h>
#include <utility>
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit dbe5384

Please sign in to comment.