Files
Horizon-OC/Source/sys-clk/overlay/lib/libultrahand/libultra/source/audio.cpp
souldbminersmwc b5022ea839 v0.18
2025-12-23 19:10:41 -05:00

312 lines
11 KiB
C++

/********************************************************************************
* File: audio.cpp
* Author: ppkantorski
* Description:
* Memory-optimized version with reduced allocation overhead and chunked I/O.
* Key changes:
* - Eliminated temporary vector allocations (saves 50% memory during load)
* - Chunked file reading to reduce peak memory usage
* - Reduced alignment padding (saves ~3-4KB per sound)
* - Added lazy loading option via unloadAllSounds()
*
* For the latest updates and contributions, visit the project's GitHub repository.
* (GitHub Repository: https://github.com/ppkantorski/Ultrahand-Overlay)
*
* Note: Please be aware that this notice cannot be altered or removed. It is a part
* of the project's documentation and must remain intact.
*
* Licensed under both GPLv2 and CC-BY-4.0
* Copyright (c) 2025 ppkantorski
********************************************************************************/
#include "audio.hpp"
namespace ult {
bool Audio::m_initialized = false;
std::atomic<bool> Audio::m_enabled{true};
float Audio::m_masterVolume = 0.6f;
bool Audio::m_lastDockedState = false;
std::vector<Audio::CachedSound> Audio::m_cachedSounds;
std::mutex Audio::m_audioMutex;
bool Audio::initialize() {
std::lock_guard<std::mutex> lock(m_audioMutex);
if (m_initialized) return true;
if (R_FAILED(audoutInitialize()) || R_FAILED(audoutStartAudioOut())) {
audoutExit();
return false;
}
m_initialized = true;
m_cachedSounds.resize(static_cast<uint32_t>(SoundType::Count));
m_lastDockedState = ult::consoleIsDocked();
reloadAllSounds();
return true;
}
void Audio::exit() {
std::lock_guard<std::mutex> lock(m_audioMutex);
// Free all cached sound buffers
for (auto& cached : m_cachedSounds) {
if (cached.buffer) {
free(cached.buffer);
cached.buffer = nullptr;
}
cached.bufferSize = 0;
cached.dataSize = 0;
}
if (m_initialized) {
audoutStopAudioOut();
audoutExit();
m_initialized = false;
}
}
void Audio::reloadAllSounds() {
for (uint32_t i = 0; i < static_cast<uint32_t>(SoundType::Count); ++i) {
loadSoundFromWav(static_cast<SoundType>(i), m_soundPaths[i]);
}
}
void Audio::unloadAllSounds(const std::initializer_list<SoundType>& excludeSounds) {
std::lock_guard<std::mutex> lock(m_audioMutex);
if (!m_initialized) return;
for (uint32_t i = 0; i < m_cachedSounds.size(); ++i) {
SoundType current = static_cast<SoundType>(i);
// Skip if this sound is in the exclude list
if (std::find(excludeSounds.begin(), excludeSounds.end(), current) != excludeSounds.end()) {
continue;
}
auto& cached = m_cachedSounds[i];
if (cached.buffer) {
free(cached.buffer);
cached.buffer = nullptr;
}
cached.bufferSize = 0;
cached.dataSize = 0;
}
}
bool Audio::reloadIfDockedChanged() {
if (!m_initialized) return false;
const bool currentDocked = ult::consoleIsDocked();
if (currentDocked == m_lastDockedState) return false;
std::lock_guard<std::mutex> lock(m_audioMutex);
m_lastDockedState = currentDocked;
reloadAllSounds();
return true;
}
bool Audio::loadSoundFromWav(SoundType type, const char* path) {
const uint32_t idx = static_cast<uint32_t>(type);
if (!m_initialized || idx >= static_cast<uint32_t>(SoundType::Count)) return false;
// Free existing buffer
free(m_cachedSounds[idx].buffer);
m_cachedSounds[idx] = { nullptr, 0, 0 };
FILE* f = fopen(path, "rb");
if (!f) return false;
// Parse WAV header
char hdr[12];
if (fread(hdr, 1, 12, f) != 12 || memcmp(hdr, "RIFF", 4) || memcmp(hdr + 8, "WAVE", 4)) {
fclose(f);
return false;
}
u16 fmt = 0, ch = 0, bits = 0;
u32 rate = 0, dSize = 0;
long dPos = 0;
// Find fmt and data chunks
while (fread(hdr, 1, 8, f) == 8) {
const u32 sz = *(u32*)(hdr + 4);
if (!memcmp(hdr, "fmt ", 4)) {
fread(&fmt, 2, 1, f);
fread(&ch, 2, 1, f);
fread(&rate, 4, 1, f);
fseek(f, 6, SEEK_CUR);
fread(&bits, 2, 1, f);
fseek(f, sz - 16, SEEK_CUR);
} else if (!memcmp(hdr, "data", 4)) {
dSize = sz;
dPos = ftell(f);
break;
} else {
fseek(f, sz, SEEK_CUR);
}
}
// Validate format
if (!dSize || fmt != 1 || ch == 0 || ch > 2 || (bits != 8 && bits != 16)) {
fclose(f);
return false;
}
// Calculate buffer sizes
// Note: audout REQUIRES stereo (2 channels), so we must duplicate mono
const bool mono = (ch == 1);
const uint32_t inSamples = dSize / (bits / 8);
const uint32_t outSamples = mono ? inSamples * 2 : inSamples; // Duplicate mono to stereo
const uint32_t outSize = outSamples * 2; // 16-bit samples
// Use smaller alignment to reduce waste (256 bytes instead of 4KB)
const uint32_t align = 0x100;
const uint32_t bufSize = (outSize + align - 1) & ~(align - 1);
// Allocate output buffer
void* buf = aligned_alloc(align, bufSize);
if (!buf) {
fclose(f);
return false;
}
fseek(f, dPos, SEEK_SET);
s16* out = (s16*)buf;
// Calculate volume scaling
float effectiveVolume = m_masterVolume;
if (m_lastDockedState) {
effectiveVolume *= 0.5f;
}
const float scale = std::clamp(effectiveVolume, 0.0f, 1.0f);
// Process audio in chunks to minimize memory usage
// This eliminates the need for temporary vectors
constexpr uint32_t CHUNK_SIZE = 512;
if (bits == 8) {
// 8-bit audio: read and convert in chunks
const int32_t scaleInt = static_cast<int32_t>(scale * 256.0f);
u8 chunk[CHUNK_SIZE];
uint32_t remaining = inSamples;
uint32_t outIdx = 0;
while (remaining > 0) {
const uint32_t toRead = std::min(remaining, CHUNK_SIZE);
if (fread(chunk, 1, toRead, f) != toRead) {
free(buf);
fclose(f);
return false;
}
for (uint32_t i = 0; i < toRead; ++i) {
const s16 sample = static_cast<s16>((chunk[i] - 128) * scaleInt);
if (mono) {
// Duplicate to both L and R channels for stereo output
out[outIdx++] = sample; // Left
out[outIdx++] = sample; // Right
} else {
out[outIdx++] = sample;
}
}
remaining -= toRead;
}
} else {
// 16-bit audio: read and convert in chunks
s16 chunk[CHUNK_SIZE];
uint32_t remaining = inSamples;
uint32_t outIdx = 0;
while (remaining > 0) {
const uint32_t toRead = std::min(remaining, CHUNK_SIZE);
if (fread(chunk, sizeof(s16), toRead, f) != toRead) {
free(buf);
fclose(f);
return false;
}
for (uint32_t i = 0; i < toRead; ++i) {
const s16 sample = static_cast<s16>(chunk[i] * scale);
if (mono) {
// Duplicate to both L and R channels for stereo output
out[outIdx++] = sample; // Left
out[outIdx++] = sample; // Right
} else {
out[outIdx++] = sample;
}
}
remaining -= toRead;
}
}
fclose(f);
// Zero-fill any padding
if (outSize < bufSize) {
memset((u8*)buf + outSize, 0, bufSize - outSize);
}
m_cachedSounds[idx] = { buf, bufSize, outSize };
return true;
}
void Audio::playSound(SoundType type) {
// Lock-free check - SAFE with atomic
if (!m_enabled.load(std::memory_order_relaxed)) return;
const uint32_t idx = static_cast<uint32_t>(type);
if (idx >= static_cast<uint32_t>(SoundType::Count)) return;
std::lock_guard<std::mutex> lock(m_audioMutex);
// Check again under lock
if (!m_initialized) return;
auto& cached = m_cachedSounds[idx];
if (!cached.buffer) return;
// Release any finished buffers
AudioOutBuffer* releasedBuffers = nullptr;
u32 releasedCount = 0;
audoutGetReleasedAudioOutBuffer(&releasedBuffers, &releasedCount);
// Static buffer is safe with mutex protection
static AudioOutBuffer audioBuffer = {};
audioBuffer = {};
audioBuffer.buffer = cached.buffer;
audioBuffer.buffer_size = cached.bufferSize;
audioBuffer.data_size = cached.dataSize;
audioBuffer.data_offset = 0;
audioBuffer.next = nullptr;
AudioOutBuffer* rel = nullptr;
audoutPlayBuffer(&audioBuffer, &rel);
}
void Audio::setMasterVolume(float v) {
std::lock_guard<std::mutex> lock(m_audioMutex);
m_masterVolume = std::clamp(v, 0.0f, 1.0f);
}
void Audio::setEnabled(bool e) {
m_enabled.store(e, std::memory_order_relaxed);
}
bool Audio::isEnabled() {
return m_enabled.load(std::memory_order_relaxed);
}
//bool Audio::isDocked() {
// Result rc = apmInitialize();
// if (R_FAILED(rc)) return false;
//
// ApmPerformanceMode perfMode = ApmPerformanceMode_Invalid;
// rc = apmGetPerformanceMode(&perfMode);
// apmExit();
//
// return R_SUCCEEDED(rc) && (perfMode == ApmPerformanceMode_Boost);
//}
}