audio: Implement cubeb audio out backend. (#1895)

* audio: Implement cubeb audio out backend.

* cubeb_audio: Add some additional safety checks.

* cubeb_audio: Add debug logging callback.

* audioout: Refactor backend ports into class.

* pthread: Bump minimum stack size to fix cubeb crash.

* cubeb_audio: Replace output yield loop with condvar.

* common: Rename ring_buffer_base to RingBuffer.
This commit is contained in:
squidbus 2024-12-27 11:04:49 -08:00 committed by GitHub
parent f95803664b
commit 333f35ef25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 733 additions and 90 deletions

4
.gitmodules vendored
View file

@ -119,3 +119,7 @@
path = externals/MoltenVK/cereal
url = https://github.com/USCiLab/cereal
shallow = true
[submodule "externals/cubeb"]
path = externals/cubeb
url = https://github.com/mozilla/cubeb
shallow = true

View file

@ -127,6 +127,7 @@ find_package(xxHash 0.8.2 MODULE)
find_package(ZLIB 1.3 MODULE)
find_package(Zydis 5.0.0 CONFIG)
find_package(pugixml 1.14 CONFIG)
find_package(cubeb CONFIG)
if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR NOT MSVC)
find_package(cryptopp 8.9.0 MODULE)
@ -198,9 +199,10 @@ set(AUDIO_LIB src/core/libraries/audio/audioin.cpp
src/core/libraries/audio/audioin.h
src/core/libraries/audio/audioout.cpp
src/core/libraries/audio/audioout.h
src/core/libraries/audio/sdl_audio.cpp
src/core/libraries/audio/sdl_audio.h
src/core/libraries/audio/audioout_backend.h
src/core/libraries/audio/audioout_error.h
src/core/libraries/audio/cubeb_audio.cpp
src/core/libraries/audio/sdl_audio.cpp
src/core/libraries/ngs2/ngs2.cpp
src/core/libraries/ngs2/ngs2.h
)
@ -493,6 +495,7 @@ set(COMMON src/common/logging/backend.cpp
src/common/polyfill_thread.h
src/common/rdtsc.cpp
src/common/rdtsc.h
src/common/ringbuffer.h
src/common/signal_context.h
src/common/signal_context.cpp
src/common/singleton.h
@ -884,7 +887,7 @@ endif()
create_target_directory_groups(shadps4)
target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 pugixml::pugixml stb::headers)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 pugixml::pugixml stb::headers cubeb::cubeb)
target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h")
target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h")

7
LICENSES/ISC.txt Normal file
View file

@ -0,0 +1,7 @@
ISC License
<copyright notice>
Permission to use, copy, modify, and /or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View file

@ -228,6 +228,16 @@ if (NOT TARGET stb::headers)
add_library(stb::headers ALIAS stb)
endif()
# cubeb
if (NOT TARGET cubeb::cubeb)
option(BUILD_TESTS "" OFF)
option(BUILD_TOOLS "" OFF)
option(BUNDLE_SPEEX "" ON)
option(USE_SANITIZERS "" OFF)
add_subdirectory(cubeb)
add_library(cubeb::cubeb ALIAS cubeb)
endif()
# Apple-only dependencies
if (APPLE)
# date

1
externals/cubeb vendored Submodule

@ -0,0 +1 @@
Subproject commit 9a9d034c51859a045a34f201334f612c51e6c19d

View file

@ -67,6 +67,7 @@ static int cursorHideTimeout = 5; // 5 seconds (default)
static bool separateupdatefolder = false;
static bool compatibilityData = false;
static bool checkCompatibilityOnStartup = false;
static std::string audioBackend = "cubeb";
// Gui
std::vector<std::filesystem::path> settings_install_dirs = {};
@ -239,6 +240,10 @@ bool getCheckCompatibilityOnStartup() {
return checkCompatibilityOnStartup;
}
std::string getAudioBackend() {
return audioBackend;
}
void setGpuId(s32 selectedGpuId) {
gpuId = selectedGpuId;
}
@ -371,6 +376,10 @@ void setCheckCompatibilityOnStartup(bool use) {
checkCompatibilityOnStartup = use;
}
void setAudioBackend(std::string backend) {
audioBackend = backend;
}
void setMainWindowGeometry(u32 x, u32 y, u32 w, u32 h) {
main_window_geometry_x = x;
main_window_geometry_y = y;
@ -611,6 +620,12 @@ void load(const std::filesystem::path& path) {
vkCrashDiagnostic = toml::find_or<bool>(vk, "crashDiagnostic", false);
}
if (data.contains("Audio")) {
const toml::value& audio = data.at("Audio");
audioBackend = toml::find_or<std::string>(audio, "backend", "cubeb");
}
if (data.contains("Debug")) {
const toml::value& debug = data.at("Debug");
@ -709,6 +724,7 @@ void save(const std::filesystem::path& path) {
data["Vulkan"]["rdocEnable"] = rdocEnable;
data["Vulkan"]["rdocMarkersEnable"] = vkMarkers;
data["Vulkan"]["crashDiagnostic"] = vkCrashDiagnostic;
data["Audio"]["backend"] = audioBackend;
data["Debug"]["DebugDump"] = isDebugDump;
data["Debug"]["CollectShader"] = isShaderDebug;
@ -812,6 +828,7 @@ void setDefaultValues() {
separateupdatefolder = false;
compatibilityData = false;
checkCompatibilityOnStartup = false;
audioBackend = "cubeb";
}
} // namespace Config

View file

@ -24,6 +24,7 @@ bool getEnableDiscordRPC();
bool getSeparateUpdateEnabled();
bool getCompatibilityEnabled();
bool getCheckCompatibilityOnStartup();
std::string getAudioBackend();
std::string getLogFilter();
std::string getLogType();
@ -75,6 +76,7 @@ void setSeparateUpdateEnabled(bool use);
void setGameInstallDirs(const std::vector<std::filesystem::path>& settings_install_dirs_config);
void setCompatibilityEnabled(bool use);
void setCheckCompatibilityOnStartup(bool use);
void setAudioBackend(std::string backend);
void setCursorState(s16 cursorState);
void setCursorHideTimeout(int newcursorHideTimeout);

374
src/common/ringbuffer.h Normal file
View file

@ -0,0 +1,374 @@
// SPDX-FileCopyrightText: Copyright 2016 Mozilla Foundation
// SPDX-License-Identifier: ISC
#pragma once
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <memory>
#include <thread>
#include "common/assert.h"
/**
* Single producer single consumer lock-free and wait-free ring buffer.
*
* This data structure allows producing data from one thread, and consuming it
* on another thread, safely and without explicit synchronization. If used on
* two threads, this data structure uses atomics for thread safety. It is
* possible to disable the use of atomics at compile time and only use this data
* structure on one thread.
*
* The role for the producer and the consumer must be constant, i.e., the
* producer should always be on one thread and the consumer should always be on
* another thread.
*
* Some words about the inner workings of this class:
* - Capacity is fixed. Only one allocation is performed, in the constructor.
* When reading and writing, the return value of the method allows checking if
* the ring buffer is empty or full.
* - We always keep the read index at least one element ahead of the write
* index, so we can distinguish between an empty and a full ring buffer: an
* empty ring buffer is when the write index is at the same position as the
* read index. A full buffer is when the write index is exactly one position
* before the read index.
* - We synchronize updates to the read index after having read the data, and
* the write index after having written the data. This means that the each
* thread can only touch a portion of the buffer that is not touched by the
* other thread.
* - Callers are expected to provide buffers. When writing to the queue,
* elements are copied into the internal storage from the buffer passed in.
* When reading from the queue, the user is expected to provide a buffer.
* Because this is a ring buffer, data might not be contiguous in memory,
* providing an external buffer to copy into is an easy way to have linear
* data for further processing.
*/
template <typename T>
class RingBuffer {
public:
/**
* Constructor for a ring buffer.
*
* This performs an allocation, but is the only allocation that will happen
* for the life time of a `RingBuffer`.
*
* @param capacity The maximum number of element this ring buffer will hold.
*/
RingBuffer(int capacity)
/* One more element to distinguish from empty and full buffer. */
: capacity_(capacity + 1) {
ASSERT(storage_capacity() < std::numeric_limits<int>::max() / 2 &&
"buffer too large for the type of index used.");
ASSERT(capacity_ > 0);
data_.reset(new T[storage_capacity()]);
/* If this queue is using atomics, initializing those members as the last
* action in the constructor acts as a full barrier, and allow capacity() to
* be thread-safe. */
write_index_ = 0;
read_index_ = 0;
}
/**
* Push `count` zero or default constructed elements in the array.
*
* Only safely called on the producer thread.
*
* @param count The number of elements to enqueue.
* @return The number of element enqueued.
*/
int enqueue_default(int count) {
return enqueue(nullptr, count);
}
/**
* @brief Put an element in the queue
*
* Only safely called on the producer thread.
*
* @param element The element to put in the queue.
*
* @return 1 if the element was inserted, 0 otherwise.
*/
int enqueue(T& element) {
return enqueue(&element, 1);
}
/**
* Push `count` elements in the ring buffer.
*
* Only safely called on the producer thread.
*
* @param elements a pointer to a buffer containing at least `count` elements.
* If `elements` is nullptr, zero or default constructed elements are
* enqueued.
* @param count The number of elements to read from `elements`
* @return The number of elements successfully coped from `elements` and
* inserted into the ring buffer.
*/
int enqueue(T* elements, int count) {
#ifndef NDEBUG
assert_correct_thread(producer_id);
#endif
int wr_idx = write_index_.load(std::memory_order_relaxed);
int rd_idx = read_index_.load(std::memory_order_acquire);
if (full_internal(rd_idx, wr_idx)) {
return 0;
}
int to_write = std::min(available_write_internal(rd_idx, wr_idx), count);
/* First part, from the write index to the end of the array. */
int first_part = std::min(storage_capacity() - wr_idx, to_write);
/* Second part, from the beginning of the array */
int second_part = to_write - first_part;
if (elements) {
Copy(data_.get() + wr_idx, elements, first_part);
Copy(data_.get(), elements + first_part, second_part);
} else {
ConstructDefault(data_.get() + wr_idx, first_part);
ConstructDefault(data_.get(), second_part);
}
write_index_.store(increment_index(wr_idx, to_write), std::memory_order_release);
return to_write;
}
/**
* Retrieve at most `count` elements from the ring buffer, and copy them to
* `elements`, if non-null.
*
* Only safely called on the consumer side.
*
* @param elements A pointer to a buffer with space for at least `count`
* elements. If `elements` is `nullptr`, `count` element will be discarded.
* @param count The maximum number of elements to dequeue.
* @return The number of elements written to `elements`.
*/
int dequeue(T* elements, int count) {
#ifndef NDEBUG
assert_correct_thread(consumer_id);
#endif
int rd_idx = read_index_.load(std::memory_order_relaxed);
int wr_idx = write_index_.load(std::memory_order_acquire);
if (empty_internal(rd_idx, wr_idx)) {
return 0;
}
int to_read = std::min(available_read_internal(rd_idx, wr_idx), count);
int first_part = std::min(storage_capacity() - rd_idx, to_read);
int second_part = to_read - first_part;
if (elements) {
Copy(elements, data_.get() + rd_idx, first_part);
Copy(elements + first_part, data_.get(), second_part);
}
read_index_.store(increment_index(rd_idx, to_read), std::memory_order_release);
return to_read;
}
/**
* Get the number of available element for consuming.
*
* Only safely called on the consumer thread.
*
* @return The number of available elements for reading.
*/
int available_read() const {
#ifndef NDEBUG
assert_correct_thread(consumer_id);
#endif
return available_read_internal(read_index_.load(std::memory_order_relaxed),
write_index_.load(std::memory_order_acquire));
}
/**
* Get the number of available elements for consuming.
*
* Only safely called on the producer thread.
*
* @return The number of empty slots in the buffer, available for writing.
*/
int available_write() const {
#ifndef NDEBUG
assert_correct_thread(producer_id);
#endif
return available_write_internal(read_index_.load(std::memory_order_acquire),
write_index_.load(std::memory_order_relaxed));
}
/**
* Get the total capacity, for this ring buffer.
*
* Can be called safely on any thread.
*
* @return The maximum capacity of this ring buffer.
*/
int capacity() const {
return storage_capacity() - 1;
}
/**
* Reset the consumer and producer thread identifier, in case the thread are
* being changed. This has to be externally synchronized. This is no-op when
* asserts are disabled.
*/
void reset_thread_ids() {
#ifndef NDEBUG
consumer_id = producer_id = std::thread::id();
#endif
}
private:
/** Return true if the ring buffer is empty.
*
* @param read_index the read index to consider
* @param write_index the write index to consider
* @return true if the ring buffer is empty, false otherwise.
**/
bool empty_internal(int read_index, int write_index) const {
return write_index == read_index;
}
/** Return true if the ring buffer is full.
*
* This happens if the write index is exactly one element behind the read
* index.
*
* @param read_index the read index to consider
* @param write_index the write index to consider
* @return true if the ring buffer is full, false otherwise.
**/
bool full_internal(int read_index, int write_index) const {
return (write_index + 1) % storage_capacity() == read_index;
}
/**
* Return the size of the storage. It is one more than the number of elements
* that can be stored in the buffer.
*
* @return the number of elements that can be stored in the buffer.
*/
int storage_capacity() const {
return capacity_;
}
/**
* Returns the number of elements available for reading.
*
* @return the number of available elements for reading.
*/
int available_read_internal(int read_index, int write_index) const {
if (write_index >= read_index) {
return write_index - read_index;
} else {
return write_index + storage_capacity() - read_index;
}
}
/**
* Returns the number of empty elements, available for writing.
*
* @return the number of elements that can be written into the array.
*/
int available_write_internal(int read_index, int write_index) const {
/* We substract one element here to always keep at least one sample
* free in the buffer, to distinguish between full and empty array. */
int rv = read_index - write_index - 1;
if (write_index >= read_index) {
rv += storage_capacity();
}
return rv;
}
/**
* Increments an index, wrapping it around the storage.
*
* @param index a reference to the index to increment.
* @param increment the number by which `index` is incremented.
* @return the new index.
*/
int increment_index(int index, int increment) const {
ASSERT(increment >= 0);
return (index + increment) % storage_capacity();
}
/**
* @brief This allows checking that enqueue (resp. dequeue) are always called
* by the right thread.
*
* @param id the id of the thread that has called the calling method first.
*/
#ifndef NDEBUG
static void assert_correct_thread(std::thread::id& id) {
if (id == std::thread::id()) {
id = std::this_thread::get_id();
return;
}
ASSERT(id == std::this_thread::get_id());
}
#endif
/** Similar to memcpy, but accounts for the size of an element. */
template <typename CopyT>
void PodCopy(CopyT* destination, const CopyT* source, size_t count) {
static_assert(std::is_trivial<CopyT>::value, "Requires trivial type");
ASSERT(destination && source);
memcpy(destination, source, count * sizeof(CopyT));
}
/** Similar to a memset to zero, but accounts for the size of an element. */
template <typename ZeroT>
void PodZero(ZeroT* destination, size_t count) {
static_assert(std::is_trivial<ZeroT>::value, "Requires trivial type");
ASSERT(destination);
memset(destination, 0, count * sizeof(ZeroT));
}
template <typename CopyT, typename Trait>
void Copy(CopyT* destination, const CopyT* source, size_t count, Trait) {
for (size_t i = 0; i < count; i++) {
destination[i] = source[i];
}
}
template <typename CopyT>
void Copy(CopyT* destination, const CopyT* source, size_t count, std::true_type) {
PodCopy(destination, source, count);
}
/**
* This allows copying a number of elements from a `source` pointer to a
* `destination` pointer, using `memcpy` if it is safe to do so, or a loop that
* calls the constructors and destructors otherwise.
*/
template <typename CopyT>
void Copy(CopyT* destination, const T* source, size_t count) {
ASSERT(destination && source);
Copy(destination, source, count, typename std::is_trivial<CopyT>::type());
}
template <typename ConstructT, typename Trait>
void ConstructDefault(ConstructT* destination, size_t count, Trait) {
for (size_t i = 0; i < count; i++) {
destination[i] = ConstructT();
}
}
template <typename ConstructT>
void ConstructDefault(ConstructT* destination, size_t count, std::true_type) {
PodZero(destination, count);
}
/**
* This allows zeroing (using memset) or default-constructing a number of
* elements calling the constructors and destructors if necessary.
*/
template <typename ConstructT>
void ConstructDefault(ConstructT* destination, size_t count) {
ASSERT(destination);
ConstructDefault(destination, count, typename std::is_arithmetic<ConstructT>::type());
}
/** Index at which the oldest element is at, in samples. */
std::atomic<int> read_index_;
/** Index at which to write new elements. `write_index` is always at
* least one element ahead of `read_index_`. */
std::atomic<int> write_index_;
/** Maximum number of elements that can be stored in the ring buffer. */
const int capacity_;
/** Data storage */
std::unique_ptr<T[]> data_;
#ifndef NDEBUG
/** The id of the only thread that is allowed to read from the queue. */
mutable std::thread::id consumer_id;
/** The id of the only thread that is allowed to write from the queue. */
mutable std::thread::id producer_id;
#endif
};

View file

@ -7,26 +7,15 @@
#include <magic_enum/magic_enum.hpp>
#include "common/assert.h"
#include "common/config.h"
#include "common/logging/log.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
#include "core/libraries/audio/audioout_error.h"
#include "core/libraries/audio/sdl_audio.h"
#include "core/libraries/libs.h"
namespace Libraries::AudioOut {
struct PortOut {
void* impl;
u32 samples_num;
u32 freq;
OrbisAudioOutParamFormat format;
OrbisAudioOutPort type;
int channels_num;
bool is_float;
std::array<int, 8> volume;
u8 sample_size;
bool is_open;
};
std::shared_mutex ports_mutex;
std::array<PortOut, SCE_AUDIO_OUT_NUM_PORTS> ports_out{};
@ -104,7 +93,7 @@ static bool IsFormatFloat(const OrbisAudioOutParamFormat format) {
}
}
static int GetFormatNumChannels(const OrbisAudioOutParamFormat format) {
static u8 GetFormatNumChannels(const OrbisAudioOutParamFormat format) {
switch (format) {
case OrbisAudioOutParamFormat::S16Mono:
case OrbisAudioOutParamFormat::FloatMono:
@ -187,13 +176,11 @@ int PS4_SYSV_ABI sceAudioOutClose(s32 handle) {
std::scoped_lock lock(ports_mutex);
auto& port = ports_out.at(handle - 1);
if (!port.is_open) {
if (!port.impl) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
audio->Close(port.impl);
port.impl = nullptr;
port.is_open = false;
return ORBIS_OK;
}
@ -264,7 +251,7 @@ int PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* sta
std::scoped_lock lock(ports_mutex);
const auto& port = ports_out.at(handle - 1);
if (!port.is_open) {
if (!port.impl) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
@ -324,7 +311,16 @@ int PS4_SYSV_ABI sceAudioOutInit() {
if (audio != nullptr) {
return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT;
}
const auto backend = Config::getAudioBackend();
if (backend == "cubeb") {
audio = std::make_unique<CubebAudioOut>();
} else if (backend == "sdl") {
audio = std::make_unique<SDLAudioOut>();
} else {
// Cubeb as a default fallback.
LOG_ERROR(Lib_AudioOut, "Invalid audio backend '{}', defaulting to cubeb.", backend);
audio = std::make_unique<CubebAudioOut>();
}
return ORBIS_OK;
}
@ -399,23 +395,25 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
}
std::scoped_lock lock{ports_mutex};
const auto port = std::ranges::find(ports_out, false, &PortOut::is_open);
const auto port =
std::ranges::find_if(ports_out, [&](const PortOut& p) { return p.impl == nullptr; });
if (port == ports_out.end()) {
LOG_ERROR(Lib_AudioOut, "Audio ports are full");
return ORBIS_AUDIO_OUT_ERROR_PORT_FULL;
}
port->is_open = true;
port->type = port_type;
port->samples_num = length;
port->freq = sample_rate;
port->format = format;
port->is_float = IsFormatFloat(format);
port->channels_num = GetFormatNumChannels(format);
port->sample_size = GetFormatSampleSize(format);
port->channels_num = GetFormatNumChannels(format);
port->samples_num = length;
port->frame_size = port->sample_size * port->channels_num;
port->buffer_size = port->frame_size * port->samples_num;
port->freq = sample_rate;
port->volume.fill(SCE_AUDIO_OUT_VOLUME_0DB);
port->impl = audio->Open(*port);
port->impl = audio->Open(port->is_float, port->channels_num, port->freq);
return std::distance(ports_out.begin(), port) + 1;
}
@ -424,7 +422,7 @@ int PS4_SYSV_ABI sceAudioOutOpenEx() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, const void* ptr) {
s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, void* ptr) {
if (handle < 1 || handle > SCE_AUDIO_OUT_NUM_PORTS) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
@ -434,12 +432,11 @@ s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, const void* ptr) {
}
auto& port = ports_out.at(handle - 1);
if (!port.is_open) {
if (!port.impl) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
const size_t data_size = port.samples_num * port.sample_size * port.channels_num;
audio->Output(port.impl, ptr, data_size);
port.impl->Output(ptr, port.buffer_size);
return ORBIS_OK;
}
@ -548,7 +545,7 @@ s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol) {
std::scoped_lock lock(ports_mutex);
auto& port = ports_out.at(handle - 1);
if (!port.is_open) {
if (!port.impl) {
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
}
@ -579,7 +576,7 @@ s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol) {
}
}
audio->SetVolume(port.impl, port.volume);
port.impl->SetVolume(port.volume);
return ORBIS_OK;
}

View file

@ -3,12 +3,15 @@
#pragma once
#include "common/bit_field.h"
#include <memory>
#include "common/bit_field.h"
#include "core/libraries/system/userservice.h"
namespace Libraries::AudioOut {
class PortBackend;
// Main up to 8 ports, BGM 1 port, voice up to 4 ports,
// personal up to 4 ports, padspk up to 5 ports, aux 1 port
constexpr int SCE_AUDIO_OUT_NUM_PORTS = 22;
@ -43,7 +46,7 @@ union OrbisAudioOutParamExtendedInformation {
struct OrbisAudioOutOutputParam {
s32 handle;
const void* ptr;
void* ptr;
};
struct OrbisAudioOutPortState {
@ -56,6 +59,21 @@ struct OrbisAudioOutPortState {
u64 reserved64[2];
};
struct PortOut {
std::unique_ptr<PortBackend> impl{};
OrbisAudioOutPort type;
OrbisAudioOutParamFormat format;
bool is_float;
u8 sample_size;
u8 channels_num;
u32 samples_num;
u32 frame_size;
u32 buffer_size;
u32 freq;
std::array<int, 8> volume;
};
int PS4_SYSV_ABI sceAudioOutDeviceIdOpen();
int PS4_SYSV_ABI sceAudioDeviceControlGet();
int PS4_SYSV_ABI sceAudioDeviceControlSet();
@ -94,7 +112,7 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
OrbisAudioOutPort port_type, s32 index, u32 length,
u32 sample_rate, OrbisAudioOutParamExtendedInformation param_type);
int PS4_SYSV_ABI sceAudioOutOpenEx();
s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, const void* ptr);
s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, void* ptr);
s32 PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num);
int PS4_SYSV_ABI sceAudioOutPtClose();
int PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime();

View file

@ -3,17 +3,42 @@
#pragma once
typedef struct cubeb cubeb;
namespace Libraries::AudioOut {
struct PortOut;
class PortBackend {
public:
virtual ~PortBackend() = default;
virtual void Output(void* ptr, size_t size) = 0;
virtual void SetVolume(const std::array<int, 8>& ch_volumes) = 0;
};
class AudioOutBackend {
public:
AudioOutBackend() = default;
virtual ~AudioOutBackend() = default;
virtual void* Open(bool is_float, int num_channels, u32 sample_rate) = 0;
virtual void Close(void* impl) = 0;
virtual void Output(void* impl, const void* ptr, size_t size) = 0;
virtual void SetVolume(void* impl, std::array<int, 8> ch_volumes) = 0;
virtual std::unique_ptr<PortBackend> Open(PortOut& port) = 0;
};
class CubebAudioOut final : public AudioOutBackend {
public:
CubebAudioOut();
~CubebAudioOut() override;
std::unique_ptr<PortBackend> Open(PortOut& port) override;
private:
cubeb* ctx = nullptr;
};
class SDLAudioOut final : public AudioOutBackend {
public:
std::unique_ptr<PortBackend> Open(PortOut& port) override;
};
} // namespace Libraries::AudioOut

View file

@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <condition_variable>
#include <mutex>
#include <cubeb/cubeb.h>
#include "common/assert.h"
#include "common/ringbuffer.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
namespace Libraries::AudioOut {
constexpr int AUDIO_STREAM_BUFFER_THRESHOLD = 65536; // Define constant for buffer threshold
class CubebPortBackend : public PortBackend {
public:
CubebPortBackend(cubeb* ctx, const PortOut& port)
: frame_size(port.frame_size), buffer(static_cast<int>(port.buffer_size) * 4) {
if (!ctx) {
return;
}
const auto get_channel_layout = [&port] -> cubeb_channel_layout {
switch (port.channels_num) {
case 1:
return CUBEB_LAYOUT_MONO;
case 2:
return CUBEB_LAYOUT_STEREO;
case 8:
return CUBEB_LAYOUT_3F4_LFE;
default:
UNREACHABLE();
}
};
cubeb_stream_params stream_params = {
.format = port.is_float ? CUBEB_SAMPLE_FLOAT32LE : CUBEB_SAMPLE_S16LE,
.rate = port.freq,
.channels = port.channels_num,
.layout = get_channel_layout(),
.prefs = CUBEB_STREAM_PREF_NONE,
};
u32 latency_frames = 512;
if (const auto ret = cubeb_get_min_latency(ctx, &stream_params, &latency_frames);
ret != CUBEB_OK) {
LOG_WARNING(Lib_AudioOut,
"Could not get minimum cubeb audio latency, falling back to default: {}",
ret);
}
char stream_name[64];
snprintf(stream_name, sizeof(stream_name), "shadPS4 stream %p", this);
if (const auto ret = cubeb_stream_init(ctx, &stream, stream_name, nullptr, nullptr, nullptr,
&stream_params, latency_frames, &DataCallback,
&StateCallback, this);
ret != CUBEB_OK) {
LOG_ERROR(Lib_AudioOut, "Failed to create cubeb stream: {}", ret);
return;
}
if (const auto ret = cubeb_stream_start(stream); ret != CUBEB_OK) {
LOG_ERROR(Lib_AudioOut, "Failed to start cubeb stream: {}", ret);
return;
}
}
~CubebPortBackend() override {
if (!stream) {
return;
}
if (const auto ret = cubeb_stream_stop(stream); ret != CUBEB_OK) {
LOG_WARNING(Lib_AudioOut, "Failed to stop cubeb stream: {}", ret);
}
cubeb_stream_destroy(stream);
stream = nullptr;
}
void Output(void* ptr, size_t size) override {
auto* data = static_cast<u8*>(ptr);
std::unique_lock lock{buffer_mutex};
buffer_cv.wait(lock, [&] { return buffer.available_write() >= size; });
buffer.enqueue(data, static_cast<int>(size));
}
void SetVolume(const std::array<int, 8>& ch_volumes) override {
if (!stream) {
return;
}
// Cubeb does not have per-channel volumes, for now just take the maximum of the channels.
const auto vol = *std::ranges::max_element(ch_volumes);
if (const auto ret =
cubeb_stream_set_volume(stream, static_cast<float>(vol) / SCE_AUDIO_OUT_VOLUME_0DB);
ret != CUBEB_OK) {
LOG_WARNING(Lib_AudioOut, "Failed to change cubeb stream volume: {}", ret);
}
}
private:
static long DataCallback(cubeb_stream* stream, void* user_data, const void* in, void* out,
long num_frames) {
auto* stream_data = static_cast<CubebPortBackend*>(user_data);
const auto out_data = static_cast<u8*>(out);
const auto requested_size = static_cast<int>(num_frames * stream_data->frame_size);
std::unique_lock lock{stream_data->buffer_mutex};
const auto dequeued_size = stream_data->buffer.dequeue(out_data, requested_size);
lock.unlock();
stream_data->buffer_cv.notify_one();
if (dequeued_size < requested_size) {
// Need to fill remaining space with silence.
std::memset(out_data + dequeued_size, 0, requested_size - dequeued_size);
}
return num_frames;
}
static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {
switch (state) {
case CUBEB_STATE_STARTED:
LOG_INFO(Lib_AudioOut, "Cubeb stream started");
break;
case CUBEB_STATE_STOPPED:
LOG_INFO(Lib_AudioOut, "Cubeb stream stopped");
break;
case CUBEB_STATE_DRAINED:
LOG_INFO(Lib_AudioOut, "Cubeb stream drained");
break;
case CUBEB_STATE_ERROR:
LOG_ERROR(Lib_AudioOut, "Cubeb stream encountered an error");
break;
}
}
size_t frame_size;
RingBuffer<u8> buffer;
std::mutex buffer_mutex;
std::condition_variable buffer_cv;
cubeb_stream* stream{};
};
CubebAudioOut::CubebAudioOut() {
if (const auto ret = cubeb_init(&ctx, "shadPS4", nullptr); ret != CUBEB_OK) {
LOG_CRITICAL(Lib_AudioOut, "Failed to create cubeb context: {}", ret);
}
}
CubebAudioOut::~CubebAudioOut() {
if (!ctx) {
return;
}
cubeb_destroy(ctx);
ctx = nullptr;
}
std::unique_ptr<PortBackend> CubebAudioOut::Open(PortOut& port) {
return std::make_unique<CubebPortBackend>(ctx, port);
}
} // namespace Libraries::AudioOut

View file

@ -1,44 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_timer.h>
#include "common/assert.h"
#include "core/libraries/audio/sdl_audio.h"
#include "common/logging/log.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
namespace Libraries::AudioOut {
constexpr int AUDIO_STREAM_BUFFER_THRESHOLD = 65536; // Define constant for buffer threshold
void* SDLAudioOut::Open(bool is_float, int num_channels, u32 sample_rate) {
SDL_AudioSpec fmt;
SDL_zero(fmt);
fmt.format = is_float ? SDL_AUDIO_F32 : SDL_AUDIO_S16;
fmt.channels = num_channels;
fmt.freq = sample_rate;
auto* stream =
class SDLPortBackend : public PortBackend {
public:
explicit SDLPortBackend(const PortOut& port) {
const SDL_AudioSpec fmt = {
.format = port.is_float ? SDL_AUDIO_F32 : SDL_AUDIO_S16,
.channels = port.channels_num,
.freq = static_cast<int>(port.freq),
};
stream =
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &fmt, nullptr, nullptr);
if (stream == nullptr) {
LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError());
}
SDL_ResumeAudioStreamDevice(stream);
return stream;
}
void SDLAudioOut::Close(void* impl) {
SDL_DestroyAudioStream(static_cast<SDL_AudioStream*>(impl));
~SDLPortBackend() override {
if (stream) {
SDL_DestroyAudioStream(stream);
stream = nullptr;
}
}
void SDLAudioOut::Output(void* impl, const void* ptr, size_t size) {
auto* stream = static_cast<SDL_AudioStream*>(impl);
SDL_PutAudioStreamData(stream, ptr, size);
void Output(void* ptr, size_t size) override {
SDL_PutAudioStreamData(stream, ptr, static_cast<int>(size));
while (SDL_GetAudioStreamAvailable(stream) > AUDIO_STREAM_BUFFER_THRESHOLD) {
SDL_Delay(0);
// Yield to allow the stream to drain.
std::this_thread::yield();
}
}
void SDLAudioOut::SetVolume(void* impl, std::array<int, 8> ch_volumes) {
// Not yet implemented
void SetVolume(const std::array<int, 8>& ch_volumes) override {
// TODO: Not yet implemented
}
private:
SDL_AudioStream* stream;
};
std::unique_ptr<PortBackend> SDLAudioOut::Open(PortOut& port) {
return std::make_unique<SDLPortBackend>(port);
}
} // namespace Libraries::AudioOut

View file

@ -1,18 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/libraries/audio/audioout_backend.h"
namespace Libraries::AudioOut {
class SDLAudioOut final : public AudioOutBackend {
public:
void* Open(bool is_float, int num_channels, u32 sample_rate) override;
void Close(void* impl) override;
void Output(void* impl, const void* ptr, size_t size) override;
void SetVolume(void* impl, std::array<int, 8> ch_volumes) override;
};
} // namespace Libraries::AudioOut

View file

@ -244,8 +244,8 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt
new_thread->tid = ++TidCounter;
if (new_thread->attr.stackaddr_attr == 0) {
/* Enforce minimum stack size of 64 KB */
static constexpr size_t MinimumStack = 64_KB;
/* Enforce minimum stack size of 128 KB */
static constexpr size_t MinimumStack = 128_KB;
auto& stacksize = new_thread->attr.stacksize_attr;
stacksize = std::max(stacksize, MinimumStack);
}

View file

@ -322,7 +322,7 @@ void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string
LOG_INFO(Loader, "No HLE available for {} module", module_name);
}
}
if (std::filesystem::exists(sys_module_path / game_serial)) {
if (!game_serial.empty() && std::filesystem::exists(sys_module_path / game_serial)) {
for (const auto& entry :
std::filesystem::directory_iterator(sys_module_path / game_serial)) {
LOG_INFO(Loader, "Loading {} from game serial file {}", entry.path().string(),

View file

@ -212,6 +212,7 @@ SettingsDialog::SettingsDialog(std::span<const QString> physical_devices,
ui->enableCompatibilityCheckBox->installEventFilter(this);
ui->checkCompatibilityOnStartupCheckBox->installEventFilter(this);
ui->updateCompatibilityButton->installEventFilter(this);
ui->audioBackendComboBox->installEventFilter(this);
// Input
ui->hideCursorGroupBox->installEventFilter(this);
@ -306,6 +307,8 @@ void SettingsDialog::LoadValuesFromConfig() {
toml::find_or<bool>(data, "General", "compatibilityEnabled", false));
ui->checkCompatibilityOnStartupCheckBox->setChecked(
toml::find_or<bool>(data, "General", "checkCompatibilityOnStartup", false));
ui->audioBackendComboBox->setCurrentText(
QString::fromStdString(toml::find_or<std::string>(data, "Audio", "backend", "cubeb")));
#ifdef ENABLE_UPDATER
ui->updateCheckBox->setChecked(toml::find_or<bool>(data, "General", "autoUpdate", false));
@ -429,6 +432,8 @@ void SettingsDialog::updateNoteTextEdit(const QString& elementName) {
text = tr("checkCompatibilityOnStartupCheckBox");
} else if (elementName == "updateCompatibilityButton") {
text = tr("updateCompatibilityButton");
} else if (elementName == "audioBackendGroupBox") {
text = tr("audioBackendGroupBox");
}
// Input
@ -544,6 +549,7 @@ void SettingsDialog::UpdateSettings() {
Config::setUpdateChannel(ui->updateComboBox->currentText().toStdString());
Config::setCompatibilityEnabled(ui->enableCompatibilityCheckBox->isChecked());
Config::setCheckCompatibilityOnStartup(ui->checkCompatibilityOnStartupCheckBox->isChecked());
Config::setAudioBackend(ui->audioBackendComboBox->currentText().toStdString());
#ifdef ENABLE_DISCORD_RPC
auto* rpc = Common::Singleton<DiscordRPCHandler::RPC>::Instance();

View file

@ -270,6 +270,29 @@
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="audioBackendGroupBox">
<property name="title">
<string>Audio Backend</string>
</property>
<layout class="QVBoxLayout" name="audioBackendBoxLayout">
<item>
<widget class="QComboBox" name="audioBackendComboBox">
<item>
<property name="text">
<string>cubeb</string>
</property>
</item>
<item>
<property name="text">
<string>sdl</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>