mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-07-10 18:24:40 -06:00
Merge ede7195046 into bffa4d1b63
This commit is contained in:
commit
bc9c6187f1
@ -47,6 +47,12 @@ enum AudioBackend : int {
|
||||
// Add more backends as needed
|
||||
};
|
||||
|
||||
enum OpenALHrtfMode : int {
|
||||
HrtfAuto, // Let OpenAL Soft decide (on for headphone-like stereo outputs)
|
||||
HrtfOn, // Force HRTF binaural rendering
|
||||
HrtfOff, // Never use HRTF
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Setting {
|
||||
T default_value{};
|
||||
@ -336,6 +342,7 @@ struct AudioSettings {
|
||||
Setting<std::string> openal_mic_device{"Default Device"};
|
||||
Setting<std::string> openal_main_output_device{"Default Device"};
|
||||
Setting<std::string> openal_padSpk_output_device{"Default Device"};
|
||||
Setting<u32> openal_hrtf{OpenALHrtfMode::HrtfAuto};
|
||||
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{
|
||||
@ -349,14 +356,15 @@ struct AudioSettings {
|
||||
make_override<AudioSettings>("openal_main_output_device",
|
||||
&AudioSettings::openal_main_output_device),
|
||||
make_override<AudioSettings>("openal_padSpk_output_device",
|
||||
&AudioSettings::openal_padSpk_output_device)};
|
||||
&AudioSettings::openal_padSpk_output_device),
|
||||
make_override<AudioSettings>("openal_hrtf", &AudioSettings::openal_hrtf)};
|
||||
}
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, audio_backend, sdl_mic_device,
|
||||
sdl_main_output_device, sdl_padSpk_output_device,
|
||||
openal_mic_device, openal_main_output_device,
|
||||
openal_padSpk_output_device)
|
||||
openal_padSpk_output_device, openal_hrtf)
|
||||
|
||||
// -------------------------------
|
||||
// GPU settings
|
||||
@ -632,6 +640,7 @@ public:
|
||||
SETTING_FORWARD(m_audio, OpenALMicDevice, openal_mic_device)
|
||||
SETTING_FORWARD(m_audio, OpenALMainOutputDevice, openal_main_output_device)
|
||||
SETTING_FORWARD(m_audio, OpenALPadSpkOutputDevice, openal_padSpk_output_device)
|
||||
SETTING_FORWARD(m_audio, OpenALHrtf, openal_hrtf)
|
||||
|
||||
// Debug settings
|
||||
SETTING_FORWARD_BOOL(m_debug, DebugDump, debug_dump)
|
||||
|
||||
@ -10,6 +10,22 @@
|
||||
#include <AL/al.h>
|
||||
#include <AL/alc.h>
|
||||
#include <alext.h>
|
||||
|
||||
// Fallbacks in case the alext.h in use predates these extensions.
|
||||
#ifndef ALC_SOFT_output_mode
|
||||
#define ALC_OUTPUT_MODE_SOFT 0x19AC
|
||||
#define ALC_MONO_SOFT 0x1500
|
||||
#define ALC_STEREO_SOFT 0x1501
|
||||
#define ALC_STEREO_BASIC_SOFT 0x19AE
|
||||
#define ALC_STEREO_UHJ_SOFT 0x19AF
|
||||
#define ALC_STEREO_HRTF_SOFT 0x19B2
|
||||
#define ALC_SURROUND_5_1_SOFT 0x1504
|
||||
#define ALC_SURROUND_6_1_SOFT 0x1505
|
||||
#define ALC_SURROUND_7_1_SOFT 0x1506
|
||||
#endif
|
||||
#ifndef AL_SOFT_direct_channels_remix
|
||||
#define AL_REMIX_UNMATCHED_SOFT 0x0002
|
||||
#endif
|
||||
#include <queue>
|
||||
#include "common/logging/log.h"
|
||||
#include "core/emulator_settings.h"
|
||||
@ -98,6 +114,10 @@ public:
|
||||
convert(ptr, al_buffer_s16.data(), buffer_frames, nullptr);
|
||||
}
|
||||
|
||||
if (fold_lfe) {
|
||||
FoldLfeIntoFronts();
|
||||
}
|
||||
|
||||
// Reclaim processed buffers
|
||||
ALint processed = 0;
|
||||
alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);
|
||||
@ -166,6 +186,7 @@ public:
|
||||
const float channel_gain = static_cast<float>(ch_volumes[i]) * INV_VOLUME_0DB;
|
||||
max_channel_gain = std::max(max_channel_gain, channel_gain);
|
||||
}
|
||||
game_gain.store(max_channel_gain, std::memory_order_release);
|
||||
|
||||
const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f;
|
||||
const float total_gain = max_channel_gain * slider_gain;
|
||||
@ -235,13 +256,48 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
ALCdevice* alc_dev = alcGetContextsDevice(alcGetCurrentContext());
|
||||
ALCint output_mode = 0;
|
||||
bool know_output_mode = false;
|
||||
if (alc_dev && alcIsExtensionPresent(alc_dev, "ALC_SOFT_output_mode")) {
|
||||
alcGetIntegerv(alc_dev, ALC_OUTPUT_MODE_SOFT, 1, &output_mode);
|
||||
know_output_mode = true;
|
||||
}
|
||||
const bool output_has_lfe = output_mode == ALC_SURROUND_5_1_SOFT ||
|
||||
output_mode == ALC_SURROUND_6_1_SOFT ||
|
||||
output_mode == ALC_SURROUND_7_1_SOFT;
|
||||
fold_lfe = know_output_mode && !output_has_lfe && num_channels >= 6 && !downmix_to_stereo;
|
||||
if (fold_lfe) {
|
||||
LOG_INFO(Lib_AudioOut, "Output has no LFE channel; folding buffer LFE into fronts");
|
||||
}
|
||||
if (alc_dev && alcIsExtensionPresent(alc_dev, "ALC_SOFT_HRTF")) {
|
||||
ALCint hrtf_on = 0;
|
||||
alcGetIntegerv(alc_dev, ALC_HRTF_SOFT, 1, &hrtf_on);
|
||||
hrtf_active = hrtf_on == ALC_TRUE;
|
||||
}
|
||||
|
||||
const bool stereo_output = output_mode == ALC_MONO_SOFT || output_mode == ALC_STEREO_SOFT ||
|
||||
output_mode == ALC_STEREO_BASIC_SOFT ||
|
||||
output_mode == ALC_STEREO_UHJ_SOFT ||
|
||||
output_mode == ALC_STEREO_HRTF_SOFT;
|
||||
if (know_output_mode && stereo_output && !hrtf_active && num_channels >= 6 &&
|
||||
!downmix_to_stereo) {
|
||||
downmix_to_stereo = true;
|
||||
use_native_float = has_float_ext;
|
||||
format = has_float_ext ? AL_FORMAT_STEREO_FLOAT32 : AL_FORMAT_STEREO16;
|
||||
fold_lfe = false; // The downmix converters already carry LFE.
|
||||
LOG_INFO(Lib_AudioOut, "Stereo output: using internal {}ch->stereo downmix ({})",
|
||||
num_channels, has_float_ext ? "float" : "int16");
|
||||
}
|
||||
|
||||
// Allocate buffers based on format
|
||||
const u32 out_channels = downmix_to_stereo ? 2u : num_channels;
|
||||
if (use_native_float) {
|
||||
al_buffer_float.resize(buffer_frames * num_channels);
|
||||
buffer_size_bytes = buffer_frames * num_channels * sizeof(float);
|
||||
al_buffer_float.resize(buffer_frames * out_channels);
|
||||
buffer_size_bytes = buffer_frames * out_channels * sizeof(float);
|
||||
} else {
|
||||
al_buffer_s16.resize(buffer_frames * num_channels);
|
||||
buffer_size_bytes = buffer_frames * num_channels * sizeof(s16);
|
||||
al_buffer_s16.resize(buffer_frames * out_channels);
|
||||
buffer_size_bytes = buffer_frames * out_channels * sizeof(s16);
|
||||
}
|
||||
|
||||
// Select optimal converter function
|
||||
@ -332,7 +388,8 @@ private:
|
||||
|
||||
last_volume_check_time = current_time;
|
||||
|
||||
const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f;
|
||||
const float config_volume =
|
||||
EmulatorSettings.GetVolumeSlider() * 0.01f * game_gain.load(std::memory_order_acquire);
|
||||
const float stored_gain = current_gain.load(std::memory_order_acquire);
|
||||
|
||||
if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) {
|
||||
@ -372,6 +429,8 @@ private:
|
||||
}
|
||||
|
||||
bool DetermineOpenALFormat() {
|
||||
alGetError();
|
||||
|
||||
// Try to use native float formats if extension is available
|
||||
if (is_float && has_float_ext) {
|
||||
switch (num_channels) {
|
||||
@ -431,6 +490,7 @@ private:
|
||||
if (format == 0 || alGetError() != AL_NO_ERROR) {
|
||||
LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo");
|
||||
format = AL_FORMAT_STEREO16;
|
||||
downmix_to_stereo = true;
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
@ -438,6 +498,7 @@ private:
|
||||
if (format == 0 || alGetError() != AL_NO_ERROR) {
|
||||
LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo");
|
||||
format = AL_FORMAT_STEREO16;
|
||||
downmix_to_stereo = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -458,6 +519,7 @@ private:
|
||||
if (format == 0 || alGetError() != AL_NO_ERROR) {
|
||||
LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo");
|
||||
format = AL_FORMAT_STEREO16;
|
||||
downmix_to_stereo = true;
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
@ -465,6 +527,7 @@ private:
|
||||
if (format == 0 || alGetError() != AL_NO_ERROR) {
|
||||
LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo");
|
||||
format = AL_FORMAT_STEREO16;
|
||||
downmix_to_stereo = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -501,11 +564,38 @@ private:
|
||||
alSourcei(source, AL_LOOPING, AL_FALSE);
|
||||
alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE);
|
||||
|
||||
if (alIsExtensionPresent("AL_SOFT_direct_channels")) {
|
||||
if (num_channels == 2 || downmix_to_stereo) {
|
||||
alSourcei(source, AL_DIRECT_CHANNELS_SOFT, AL_TRUE);
|
||||
} else if (num_channels > 2 && !hrtf_active &&
|
||||
alIsExtensionPresent("AL_SOFT_direct_channels_remix")) {
|
||||
alSourcei(source, AL_DIRECT_CHANNELS_SOFT, AL_REMIX_UNMATCHED_SOFT);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(Lib_AudioOut, "Created OpenAL source {} with {} buffers", source, buffers.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectConverter() {
|
||||
if (downmix_to_stereo) {
|
||||
if (use_native_float) {
|
||||
if (is_float) {
|
||||
convert =
|
||||
num_channels == 8 ? &DownmixF32_8CHToStereoF32 : &DownmixF32_6CHToStereoF32;
|
||||
} else {
|
||||
convert =
|
||||
num_channels == 8 ? &DownmixS16_8CHToStereoF32 : &DownmixS16_6CHToStereoF32;
|
||||
}
|
||||
} else if (is_float) {
|
||||
convert =
|
||||
num_channels == 8 ? &DownmixF32_8CHToStereoS16 : &DownmixF32_6CHToStereoS16;
|
||||
} else {
|
||||
convert = num_channels == 8 ? &DownmixS16_8CHToStereo : &DownmixS16_6CHToStereo;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_float && use_native_float) {
|
||||
// Native float - just copy/remap if needed
|
||||
switch (num_channels) {
|
||||
@ -556,7 +646,7 @@ private:
|
||||
convert = &ConvertS16Stereo;
|
||||
break;
|
||||
case 8:
|
||||
convert = &ConvertS16_8CH;
|
||||
convert = is_std ? &ConvertS16Std8CH : &ConvertS16_8CH;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels);
|
||||
@ -624,6 +714,146 @@ private:
|
||||
const u32 num_samples = frames << 3;
|
||||
std::memcpy(d, s, num_samples * sizeof(s16));
|
||||
}
|
||||
static void ConvertS16Std8CH(const void* src, void* dst, u32 frames, const float*) {
|
||||
const s16* s = static_cast<const s16*>(src);
|
||||
s16* d = static_cast<s16*>(dst);
|
||||
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 offset = i << 3;
|
||||
d[offset + FL] = s[offset + FL];
|
||||
d[offset + FR] = s[offset + FR];
|
||||
d[offset + FC] = s[offset + FC];
|
||||
d[offset + LF] = s[offset + LF];
|
||||
d[offset + SL] = s[offset + STD_SL];
|
||||
d[offset + SR] = s[offset + STD_SR];
|
||||
d[offset + BL] = s[offset + STD_BL];
|
||||
d[offset + BR] = s[offset + STD_BR];
|
||||
}
|
||||
}
|
||||
|
||||
static inline s16 ClampSampleToS16(const float v) {
|
||||
return static_cast<s16>(std::clamp(v, -32768.0f, 32767.0f));
|
||||
}
|
||||
|
||||
static void DownmixS16_6CHToStereo(const void* src, void* dst, u32 frames, const float*) {
|
||||
const s16* s = static_cast<const s16*>(src);
|
||||
s16* d = static_cast<s16*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i * 6;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] = ClampSampleToS16(s[o + FL] + center + lfe + 0.7071f * s[o + 4]);
|
||||
d[i * 2 + 1] = ClampSampleToS16(s[o + FR] + center + lfe + 0.7071f * s[o + 5]);
|
||||
}
|
||||
}
|
||||
static void DownmixS16_8CHToStereo(const void* src, void* dst, u32 frames, const float*) {
|
||||
const s16* s = static_cast<const s16*>(src);
|
||||
s16* d = static_cast<s16*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i << 3;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] =
|
||||
ClampSampleToS16(s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6]));
|
||||
d[i * 2 + 1] =
|
||||
ClampSampleToS16(s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7]));
|
||||
}
|
||||
}
|
||||
static void DownmixF32_6CHToStereoS16(const void* src, void* dst, u32 frames, const float*) {
|
||||
const float* s = static_cast<const float*>(src);
|
||||
s16* d = static_cast<s16*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i * 6;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] = OrbisFloatToS16(s[o + FL] + center + lfe + 0.7071f * s[o + 4]);
|
||||
d[i * 2 + 1] = OrbisFloatToS16(s[o + FR] + center + lfe + 0.7071f * s[o + 5]);
|
||||
}
|
||||
}
|
||||
|
||||
void FoldLfeIntoFronts() {
|
||||
constexpr float LFE_GAIN = 0.7071f;
|
||||
if (use_native_float) {
|
||||
float* d = al_buffer_float.data();
|
||||
for (u32 i = 0; i < buffer_frames; i++) {
|
||||
float* f = d + static_cast<size_t>(i) * num_channels;
|
||||
const float lfe = f[LF] * LFE_GAIN;
|
||||
f[FL] += lfe;
|
||||
f[FR] += lfe;
|
||||
f[LF] = 0.0f;
|
||||
}
|
||||
} else {
|
||||
s16* d = al_buffer_s16.data();
|
||||
for (u32 i = 0; i < buffer_frames; i++) {
|
||||
s16* f = d + static_cast<size_t>(i) * num_channels;
|
||||
const float lfe = static_cast<float>(f[LF]) * LFE_GAIN;
|
||||
f[FL] = ClampSampleToS16(static_cast<float>(f[FL]) + lfe);
|
||||
f[FR] = ClampSampleToS16(static_cast<float>(f[FR]) + lfe);
|
||||
f[LF] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void DownmixS16_6CHToStereoF32(const void* src, void* dst, u32 frames, const float*) {
|
||||
constexpr float INV = 1.0f / 32768.0f;
|
||||
const s16* s = static_cast<const s16*>(src);
|
||||
float* d = static_cast<float*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i * 6;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] = (s[o + FL] + center + lfe + 0.7071f * s[o + 4]) * INV;
|
||||
d[i * 2 + 1] = (s[o + FR] + center + lfe + 0.7071f * s[o + 5]) * INV;
|
||||
}
|
||||
}
|
||||
static void DownmixS16_8CHToStereoF32(const void* src, void* dst, u32 frames, const float*) {
|
||||
constexpr float INV = 1.0f / 32768.0f;
|
||||
const s16* s = static_cast<const s16*>(src);
|
||||
float* d = static_cast<float*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i << 3;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] = (s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6])) * INV;
|
||||
d[i * 2 + 1] = (s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7])) * INV;
|
||||
}
|
||||
}
|
||||
static void DownmixF32_6CHToStereoF32(const void* src, void* dst, u32 frames, const float*) {
|
||||
const float* s = static_cast<const float*>(src);
|
||||
float* d = static_cast<float*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i * 6;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] = s[o + FL] + center + lfe + 0.7071f * s[o + 4];
|
||||
d[i * 2 + 1] = s[o + FR] + center + lfe + 0.7071f * s[o + 5];
|
||||
}
|
||||
}
|
||||
static void DownmixF32_8CHToStereoF32(const void* src, void* dst, u32 frames, const float*) {
|
||||
const float* s = static_cast<const float*>(src);
|
||||
float* d = static_cast<float*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i << 3;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] = s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6]);
|
||||
d[i * 2 + 1] = s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7]);
|
||||
}
|
||||
}
|
||||
|
||||
static void DownmixF32_8CHToStereoS16(const void* src, void* dst, u32 frames, const float*) {
|
||||
const float* s = static_cast<const float*>(src);
|
||||
s16* d = static_cast<s16*>(dst);
|
||||
for (u32 i = 0; i < frames; i++) {
|
||||
const u32 o = i << 3;
|
||||
const float center = 0.7071f * s[o + FC];
|
||||
const float lfe = 0.5f * s[o + LF];
|
||||
d[i * 2 + 0] =
|
||||
OrbisFloatToS16(s[o + FL] + center + lfe + 0.7071f * (s[o + 4] + s[o + 6]));
|
||||
d[i * 2 + 1] =
|
||||
OrbisFloatToS16(s[o + FR] + center + lfe + 0.7071f * (s[o + 5] + s[o + 7]));
|
||||
}
|
||||
}
|
||||
|
||||
// Float passthrough converters (for AL_EXT_FLOAT32)
|
||||
static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) {
|
||||
@ -814,12 +1044,16 @@ private:
|
||||
// Extension support
|
||||
bool has_float_ext{false};
|
||||
bool use_native_float{false};
|
||||
bool downmix_to_stereo{false};
|
||||
bool fold_lfe{false};
|
||||
bool hrtf_active{false};
|
||||
|
||||
// Converter function pointer
|
||||
ConverterFunc convert{nullptr};
|
||||
|
||||
// Volume management
|
||||
alignas(64) std::atomic<float> current_gain{1.0f};
|
||||
std::atomic<float> game_gain{1.0f};
|
||||
|
||||
std::string device_name;
|
||||
bool device_registered;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@ -8,6 +9,22 @@
|
||||
#include <AL/al.h>
|
||||
#include <AL/alc.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/emulator_settings.h"
|
||||
|
||||
// ALC_SOFT_HRTF constants, in case the alext.h in use predates the extension.
|
||||
#ifndef ALC_SOFT_HRTF
|
||||
#define ALC_HRTF_SOFT 0x1992
|
||||
#define ALC_DONT_CARE_SOFT 0x0002
|
||||
#define ALC_HRTF_STATUS_SOFT 0x1993
|
||||
#define ALC_HRTF_DISABLED_SOFT 0x0000
|
||||
#define ALC_HRTF_ENABLED_SOFT 0x0001
|
||||
#define ALC_HRTF_DENIED_SOFT 0x0002
|
||||
#define ALC_HRTF_REQUIRED_SOFT 0x0003
|
||||
#define ALC_HRTF_HEADPHONES_DETECTED_SOFT 0x0004
|
||||
#define ALC_HRTF_UNSUPPORTED_FORMAT_SOFT 0x0005
|
||||
#endif
|
||||
|
||||
namespace Libraries::AudioOut {
|
||||
|
||||
struct DeviceContext {
|
||||
@ -196,8 +213,23 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create context
|
||||
ctx.context = alcCreateContext(ctx.device, nullptr);
|
||||
std::array<ALCint, 5> attrs{};
|
||||
std::size_t attr_count = 0;
|
||||
attrs[attr_count++] = ALC_FREQUENCY;
|
||||
attrs[attr_count++] = 48000;
|
||||
|
||||
const bool has_hrtf_ext = alcIsExtensionPresent(ctx.device, "ALC_SOFT_HRTF");
|
||||
if (has_hrtf_ext) {
|
||||
const u32 hrtf_mode = EmulatorSettings.GetOpenALHrtf();
|
||||
const ALCint hrtf_value = hrtf_mode == OpenALHrtfMode::HrtfOn ? ALC_TRUE
|
||||
: hrtf_mode == OpenALHrtfMode::HrtfOff ? ALC_FALSE
|
||||
: ALC_DONT_CARE_SOFT;
|
||||
attrs[attr_count++] = ALC_HRTF_SOFT;
|
||||
attrs[attr_count++] = hrtf_value;
|
||||
}
|
||||
attrs[attr_count] = 0;
|
||||
|
||||
ctx.context = alcCreateContext(ctx.device, attrs.data());
|
||||
if (!ctx.context) {
|
||||
LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context");
|
||||
alcCloseDevice(ctx.device);
|
||||
@ -213,11 +245,47 @@ private:
|
||||
actual_name = alcGetString(ctx.device, ALC_DEVICE_SPECIFIER);
|
||||
}
|
||||
ctx.device_name = actual_name ? actual_name : "Unknown";
|
||||
ALCint mixer_rate = 0;
|
||||
alcGetIntegerv(ctx.device, ALC_FREQUENCY, 1, &mixer_rate);
|
||||
LOG_INFO(Lib_AudioOut, "OpenAL mixer rate for '{}': {} Hz", ctx.device_name, mixer_rate);
|
||||
if (mixer_rate != 0 && mixer_rate != 48000) {
|
||||
LOG_WARNING(Lib_AudioOut,
|
||||
"OpenAL mixer is not running at 48000 Hz per-source resampling active");
|
||||
}
|
||||
|
||||
if (has_hrtf_ext) {
|
||||
ALCint status = ALC_HRTF_DISABLED_SOFT;
|
||||
alcGetIntegerv(ctx.device, ALC_HRTF_STATUS_SOFT, 1, &status);
|
||||
LOG_INFO(Lib_AudioOut, "OpenAL HRTF status for '{}': {}", ctx.device_name,
|
||||
HrtfStatusString(status));
|
||||
} else {
|
||||
LOG_INFO(Lib_AudioOut, "OpenAL device '{}' does not support ALC_SOFT_HRTF",
|
||||
ctx.device_name);
|
||||
}
|
||||
|
||||
LOG_INFO(Lib_AudioOut, "OpenAL device initialized: '{}'", ctx.device_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* HrtfStatusString(const ALCint status) {
|
||||
switch (status) {
|
||||
case ALC_HRTF_DISABLED_SOFT:
|
||||
return "disabled";
|
||||
case ALC_HRTF_ENABLED_SOFT:
|
||||
return "enabled";
|
||||
case ALC_HRTF_DENIED_SOFT:
|
||||
return "denied by configuration";
|
||||
case ALC_HRTF_REQUIRED_SOFT:
|
||||
return "required by configuration";
|
||||
case ALC_HRTF_HEADPHONES_DETECTED_SOFT:
|
||||
return "enabled (headphones detected)";
|
||||
case ALC_HRTF_UNSUPPORTED_FORMAT_SOFT:
|
||||
return "unsupported output format";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, DeviceContext> devices;
|
||||
mutable std::mutex mutex;
|
||||
ALCcontext* current_context{nullptr}; // For thread-local tracking
|
||||
|
||||
@ -109,6 +109,7 @@ public:
|
||||
const float channel_gain = static_cast<float>(ch_volumes[i]) * INV_VOLUME_0DB;
|
||||
max_channel_gain = std::max(max_channel_gain, channel_gain);
|
||||
}
|
||||
game_gain.store(max_channel_gain, std::memory_order_release);
|
||||
|
||||
const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; // Faster than /100.0f
|
||||
const float total_gain = max_channel_gain * slider_gain;
|
||||
@ -201,7 +202,8 @@ private:
|
||||
|
||||
last_volume_check_time = current_time;
|
||||
|
||||
const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f;
|
||||
const float config_volume =
|
||||
EmulatorSettings.GetVolumeSlider() * 0.01f * game_gain.load(std::memory_order_acquire);
|
||||
const float stored_gain = current_gain.load(std::memory_order_acquire);
|
||||
|
||||
// Only update if the difference is significant
|
||||
@ -587,6 +589,7 @@ private:
|
||||
|
||||
// Volume management
|
||||
alignas(64) std::atomic<float> current_gain{1.0f};
|
||||
std::atomic<float> game_gain{1.0f};
|
||||
|
||||
// SDL audio stream
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
|
||||
@ -24,15 +24,67 @@ static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2;
|
||||
|
||||
static std::unique_ptr<Audio3dState> state;
|
||||
|
||||
struct AudioOutBufferInfo {
|
||||
u32 channels;
|
||||
u32 sample_size;
|
||||
};
|
||||
|
||||
static AudioOutBufferInfo GetAudioOutBufferInfo(const AudioOut::OrbisAudioOutParamFormat format) {
|
||||
using Format = AudioOut::OrbisAudioOutParamFormat;
|
||||
switch (format) {
|
||||
case Format::S16Mono:
|
||||
return {1, sizeof(s16)};
|
||||
case Format::S16Stereo:
|
||||
return {2, sizeof(s16)};
|
||||
case Format::S16_8CH:
|
||||
case Format::S16_8CH_Std:
|
||||
return {8, sizeof(s16)};
|
||||
case Format::FloatMono:
|
||||
return {1, sizeof(float)};
|
||||
case Format::FloatStereo:
|
||||
return {2, sizeof(float)};
|
||||
case Format::Float_8CH:
|
||||
case Format::Float_8CH_Std:
|
||||
return {8, sizeof(float)};
|
||||
default:
|
||||
return {2, sizeof(s16)};
|
||||
}
|
||||
}
|
||||
|
||||
static s32 DrainAssociatedPorts(Port& port) {
|
||||
while (true) {
|
||||
s32 handle = -1;
|
||||
std::vector<u8> buffer;
|
||||
{
|
||||
std::scoped_lock lock{port.mutex};
|
||||
const auto it =
|
||||
std::find_if(port.audioout_ports.begin(), port.audioout_ports.end(),
|
||||
[](const AssociatedAudioOutPort& p) { return !p.pending.empty(); });
|
||||
if (it == port.audioout_ports.end()) {
|
||||
return ORBIS_OK;
|
||||
}
|
||||
handle = it->handle;
|
||||
buffer = std::move(it->pending.front());
|
||||
it->pending.pop_front();
|
||||
}
|
||||
|
||||
const s32 ret = AudioOut::sceAudioOutOutput(handle, buffer.data());
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) {
|
||||
LOG_INFO(Lib_Audio3d, "called, handle = {}", handle);
|
||||
|
||||
// Remove from any port that was tracking this handle.
|
||||
// Remove from any port that was tracking this handle. Pending buffers that
|
||||
// were never pushed are discarded, matching an immediate close.
|
||||
if (state) {
|
||||
for (auto& [port_id, port] : state->ports) {
|
||||
std::scoped_lock lock{port.mutex};
|
||||
auto& handles = port.audioout_handles;
|
||||
handles.erase(std::remove(handles.begin(), handles.end(), handle), handles.end());
|
||||
std::erase_if(port.audioout_ports,
|
||||
[&](const AssociatedAudioOutPort& p) { return p.handle == handle; });
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,8 +116,12 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Track this handle in the port so sceAudio3dPortFlush can use it for sync.
|
||||
state->ports[port_id].audioout_handles.push_back(handle);
|
||||
const auto info = GetAudioOutBufferInfo(param.data_format.Value());
|
||||
AssociatedAudioOutPort aout{};
|
||||
aout.handle = handle;
|
||||
aout.buffer_bytes = len * info.channels * info.sample_size;
|
||||
aout.samples_per_buffer = len * info.channels;
|
||||
state->ports[port_id].audioout_ports.push_back(std::move(aout));
|
||||
return handle;
|
||||
}
|
||||
|
||||
@ -82,6 +138,36 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) {
|
||||
return ORBIS_AUDIO3D_ERROR_INVALID_PORT;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
for (auto& [port_id, port] : state->ports) {
|
||||
std::scoped_lock lock{port.mutex};
|
||||
for (auto& aout : port.audioout_ports) {
|
||||
if (aout.handle != handle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (aout.pending.size() >= port.parameters.queue_depth) {
|
||||
LOG_DEBUG(Lib_Audio3d,
|
||||
"AudioOut handle {} queue full ({}) without Push, "
|
||||
"submitting oldest",
|
||||
handle, aout.pending.size());
|
||||
std::vector<u8> oldest = std::move(aout.pending.front());
|
||||
aout.pending.pop_front();
|
||||
const s32 ret = AudioOut::sceAudioOutOutput(handle, oldest.data());
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
const u8* src = static_cast<const u8*>(ptr);
|
||||
aout.pending.emplace_back(src, src + aout.buffer_bytes);
|
||||
|
||||
// Mirror sceAudioOutOutput's return of samples sent.
|
||||
return static_cast<s32>(aout.samples_per_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AudioOut::sceAudioOutOutput(handle, ptr);
|
||||
}
|
||||
|
||||
@ -94,7 +180,14 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* p
|
||||
return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
return AudioOut::sceAudioOutOutputs(param, num);
|
||||
for (u32 i = 0; i < num; i++) {
|
||||
const s32 ret = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
static s32 ConvertAndEnqueue(std::deque<AudioData>& queue, const OrbisAudio3dPcm& pcm,
|
||||
@ -593,10 +686,10 @@ s32 PS4_SYSV_ABI sceAudio3dPortClose(const OrbisAudio3dPortId port_id) {
|
||||
port.audio_out_handle = -1;
|
||||
}
|
||||
|
||||
for (const s32 handle : port.audioout_handles) {
|
||||
AudioOut::sceAudioOutClose(handle);
|
||||
for (const auto& aout : port.audioout_ports) {
|
||||
AudioOut::sceAudioOutClose(aout.handle);
|
||||
}
|
||||
port.audioout_handles.clear();
|
||||
port.audioout_ports.clear();
|
||||
|
||||
for (auto& data : port.mixed_queue) {
|
||||
std::free(data.sample_buffer);
|
||||
@ -652,9 +745,12 @@ s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) {
|
||||
auto& port = state->ports[port_id];
|
||||
std::scoped_lock lock{port.mutex};
|
||||
|
||||
if (!port.audioout_handles.empty()) {
|
||||
for (const s32 handle : port.audioout_handles) {
|
||||
const s32 ret = AudioOut::sceAudioOutOutput(handle, nullptr);
|
||||
if (!port.audioout_ports.empty()) {
|
||||
if (const s32 ret = DrainAssociatedPorts(port); ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
for (const auto& aout : port.audioout_ports) {
|
||||
const s32 ret = AudioOut::sceAudioOutOutput(aout.handle, nullptr);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
@ -957,6 +1053,10 @@ s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id,
|
||||
|
||||
const u32 depth = port.parameters.queue_depth;
|
||||
|
||||
if (const s32 ret = DrainAssociatedPorts(port); ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (port.audio_out_handle < 0) {
|
||||
AudioOut::OrbisAudioOutParamExtendedInformation ext_info{};
|
||||
ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT);
|
||||
|
||||
@ -103,13 +103,21 @@ struct ObjectState {
|
||||
std::unordered_map<u32, std::vector<u8>> persistent_attributes;
|
||||
};
|
||||
|
||||
// An AudioOut port opened by the game through sceAudio3dAudioOutOpen.
|
||||
struct AssociatedAudioOutPort {
|
||||
s32 handle{-1};
|
||||
u32 buffer_bytes{0};
|
||||
u32 samples_per_buffer{0};
|
||||
std::deque<std::vector<u8>> pending;
|
||||
};
|
||||
|
||||
struct Port {
|
||||
mutable std::recursive_mutex mutex;
|
||||
OrbisAudio3dOpenParameters parameters{};
|
||||
// Opened lazily on the first sceAudio3dPortPush call.
|
||||
s32 audio_out_handle{-1};
|
||||
// Handles explicitly opened by the game via sceAudio3dAudioOutOpen.
|
||||
std::vector<s32> audioout_handles;
|
||||
// AudioOut ports explicitly opened by the game via sceAudio3dAudioOutOpen.
|
||||
std::vector<AssociatedAudioOutPort> audioout_ports;
|
||||
// Reserved objects and their state.
|
||||
std::unordered_map<OrbisAudio3dObjectId, ObjectState> objects;
|
||||
// increasing counter for generating unique object IDs within this port.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
|
||||
@ -82,6 +83,12 @@ enum class OrbisAudio3dAttributeId : u32 {
|
||||
ORBIS_AUDIO3D_ATTRIBUTE_OUTPUT_ROUTE = 11,
|
||||
};
|
||||
|
||||
enum class OrbisAudio3dPortAttributeId : u32 {
|
||||
ORBIS_AUDIO3D_PORT_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001,
|
||||
ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002,
|
||||
ORBIS_AUDIO3D_PORT_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003,
|
||||
};
|
||||
|
||||
struct OrbisAudio3dAttribute {
|
||||
OrbisAudio3dAttributeId attribute_id;
|
||||
int : 32;
|
||||
@ -96,9 +103,50 @@ struct AudioData {
|
||||
OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16};
|
||||
};
|
||||
|
||||
struct OrbisAudio3dPosition {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
};
|
||||
|
||||
enum class OrbisAudio3dPassthrough : u32 {
|
||||
ORBIS_AUDIO3D_PASSTHROUGH_NONE = 0,
|
||||
ORBIS_AUDIO3D_PASSTHROUGH_LEFT = 1,
|
||||
ORBIS_AUDIO3D_PASSTHROUGH_RIGHT = 2,
|
||||
};
|
||||
|
||||
struct SpatialSource {
|
||||
u32 source{0};
|
||||
std::vector<u32> buffers; // ALuint ring, sized queue_depth + slack
|
||||
std::vector<u32> available; // reclaimed / never-queued buffer ids
|
||||
};
|
||||
|
||||
struct ObjectState {
|
||||
std::deque<AudioData> pcm_queue;
|
||||
std::unordered_map<u32, std::vector<u8>> persistent_attributes;
|
||||
SpatialSource al;
|
||||
};
|
||||
|
||||
struct SpatialObjectFrame {
|
||||
OrbisAudio3dObjectId object_id{};
|
||||
AudioData pcm{};
|
||||
float gain{0.0f};
|
||||
OrbisAudio3dPosition position{0.0f, 0.0f, 0.0f};
|
||||
bool has_position{false};
|
||||
float spread{0.0f};
|
||||
OrbisAudio3dPassthrough passthrough{OrbisAudio3dPassthrough::ORBIS_AUDIO3D_PASSTHROUGH_NONE};
|
||||
};
|
||||
|
||||
struct SpatialFrameBundle {
|
||||
AudioData bed{};
|
||||
std::vector<SpatialObjectFrame> objects;
|
||||
};
|
||||
|
||||
struct AssociatedAudioOutPort {
|
||||
s32 handle{-1};
|
||||
u32 buffer_bytes{0};
|
||||
u32 samples_per_buffer{0};
|
||||
std::deque<std::vector<u8>> pending;
|
||||
};
|
||||
|
||||
struct Port {
|
||||
@ -106,16 +154,37 @@ struct Port {
|
||||
OrbisAudio3dOpenParameters parameters{};
|
||||
// Opened lazily on the first sceAudio3dPortPush call.
|
||||
s32 audio_out_handle{-1};
|
||||
// Handles explicitly opened by the game via sceAudio3dAudioOutOpen.
|
||||
std::vector<s32> audioout_handles;
|
||||
// AudioOut ports explicitly opened by the game via sceAudio3dAudioOutOpen.
|
||||
std::vector<AssociatedAudioOutPort> audioout_ports;
|
||||
// Reserved objects and their state.
|
||||
std::unordered_map<OrbisAudio3dObjectId, ObjectState> objects;
|
||||
// Increasing counter for generating unique object IDs within this port.
|
||||
OrbisAudio3dObjectId next_object_id{0};
|
||||
// Bed audio queue.
|
||||
std::deque<AudioData> bed_queue;
|
||||
// Mixed stereo frames ready to be consumed by sceAudio3dPortPush.
|
||||
// Mixed stereo frames ready to be consumed by sceAudio3dPortPush
|
||||
// (CPU-mix fallback path only).
|
||||
std::deque<AudioData> mixed_queue;
|
||||
// Per-tick frame bundles produced by PortAdvance in the spatial path.
|
||||
std::deque<SpatialFrameBundle> spatial_queue;
|
||||
|
||||
// Spatial (direct OpenAL) output state.
|
||||
bool spatial_init_attempted{false};
|
||||
bool spatial_ready{false};
|
||||
bool source_radius_supported{false};
|
||||
bool direct_channels_supported{false};
|
||||
std::string device_name;
|
||||
u64 period_us{0};
|
||||
u64 last_volume_check_us{0};
|
||||
float current_gain{-1.0f};
|
||||
SpatialSource bed;
|
||||
std::vector<s16> spatial_scratch;
|
||||
std::vector<s16> spatial_scratch_stereo;
|
||||
// EFX late reverb
|
||||
bool reverb_supported{false};
|
||||
u32 reverb_slot{0}; // ALuint
|
||||
u32 reverb_effect{0}; // ALuint
|
||||
float late_reverb_level{0.0f};
|
||||
};
|
||||
|
||||
struct Audio3dState {
|
||||
@ -175,8 +244,8 @@ s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUser
|
||||
s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking);
|
||||
s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug();
|
||||
s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(OrbisAudio3dPortId port_id,
|
||||
OrbisAudio3dAttributeId attribute_id, void* attribute,
|
||||
u64 attribute_size);
|
||||
OrbisAudio3dPortAttributeId attribute_id,
|
||||
void* attribute, u64 attribute_size);
|
||||
s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler();
|
||||
s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler();
|
||||
s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user