mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2025-01-17 04:06:25 +00:00
audio: Accurate audio output timing. (#1986)
* audio: Accurate audio output timing. * audio: Handle SDL audio queue stalls. * audio: Format info cleanup.
This commit is contained in:
parent
927dc6d95c
commit
48c51bd9ef
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -119,7 +119,3 @@
|
|||
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
|
||||
|
|
|
@ -127,7 +127,6 @@ 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)
|
||||
|
@ -203,7 +202,6 @@ set(AUDIO_LIB src/core/libraries/audio/audioin.cpp
|
|||
src/core/libraries/audio/audioout.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
|
||||
|
@ -499,7 +497,6 @@ 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
|
||||
|
@ -892,7 +889,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 cubeb::cubeb)
|
||||
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_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")
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
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.
|
10
externals/CMakeLists.txt
vendored
10
externals/CMakeLists.txt
vendored
|
@ -228,16 +228,6 @@ 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
1
externals/cubeb
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit 9a9d034c51859a045a34f201334f612c51e6c19d
|
|
@ -67,7 +67,6 @@ 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 = {};
|
||||
|
@ -240,10 +239,6 @@ bool getCheckCompatibilityOnStartup() {
|
|||
return checkCompatibilityOnStartup;
|
||||
}
|
||||
|
||||
std::string getAudioBackend() {
|
||||
return audioBackend;
|
||||
}
|
||||
|
||||
void setGpuId(s32 selectedGpuId) {
|
||||
gpuId = selectedGpuId;
|
||||
}
|
||||
|
@ -376,10 +371,6 @@ 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;
|
||||
|
@ -620,12 +611,6 @@ 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");
|
||||
|
||||
|
@ -724,7 +709,6 @@ 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;
|
||||
|
||||
|
@ -828,7 +812,6 @@ void setDefaultValues() {
|
|||
separateupdatefolder = false;
|
||||
compatibilityData = false;
|
||||
checkCompatibilityOnStartup = false;
|
||||
audioBackend = "cubeb";
|
||||
}
|
||||
|
||||
} // namespace Config
|
||||
|
|
|
@ -24,7 +24,6 @@ bool getEnableDiscordRPC();
|
|||
bool getSeparateUpdateEnabled();
|
||||
bool getCompatibilityEnabled();
|
||||
bool getCheckCompatibilityOnStartup();
|
||||
std::string getAudioBackend();
|
||||
|
||||
std::string getLogFilter();
|
||||
std::string getLogType();
|
||||
|
@ -76,7 +75,6 @@ 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);
|
||||
|
|
|
@ -1,374 +0,0 @@
|
|||
// 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
|
||||
};
|
|
@ -9,6 +9,8 @@
|
|||
#include "common/assert.h"
|
||||
#include "common/config.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "common/thread.h"
|
||||
#include "core/libraries/audio/audioout.h"
|
||||
#include "core/libraries/audio/audioout_backend.h"
|
||||
#include "core/libraries/audio/audioout_error.h"
|
||||
|
@ -21,111 +23,28 @@ std::array<PortOut, SCE_AUDIO_OUT_NUM_PORTS> ports_out{};
|
|||
|
||||
static std::unique_ptr<AudioOutBackend> audio;
|
||||
|
||||
static std::string_view GetAudioOutPort(OrbisAudioOutPort port) {
|
||||
switch (port) {
|
||||
case OrbisAudioOutPort::Main:
|
||||
return "MAIN";
|
||||
case OrbisAudioOutPort::Bgm:
|
||||
return "BGM";
|
||||
case OrbisAudioOutPort::Voice:
|
||||
return "VOICE";
|
||||
case OrbisAudioOutPort::Personal:
|
||||
return "PERSONAL";
|
||||
case OrbisAudioOutPort::Padspk:
|
||||
return "PADSPK";
|
||||
case OrbisAudioOutPort::Aux:
|
||||
return "AUX";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string_view GetAudioOutParamFormat(OrbisAudioOutParamFormat param) {
|
||||
switch (param) {
|
||||
case OrbisAudioOutParamFormat::S16Mono:
|
||||
return "S16_MONO";
|
||||
case OrbisAudioOutParamFormat::S16Stereo:
|
||||
return "S16_STEREO";
|
||||
case OrbisAudioOutParamFormat::S16_8CH:
|
||||
return "S16_8CH";
|
||||
case OrbisAudioOutParamFormat::FloatMono:
|
||||
return "FLOAT_MONO";
|
||||
case OrbisAudioOutParamFormat::FloatStereo:
|
||||
return "FLOAT_STEREO";
|
||||
case OrbisAudioOutParamFormat::Float_8CH:
|
||||
return "FLOAT_8CH";
|
||||
case OrbisAudioOutParamFormat::S16_8CH_Std:
|
||||
return "S16_8CH_STD";
|
||||
case OrbisAudioOutParamFormat::Float_8CH_Std:
|
||||
return "FLOAT_8CH_STD";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string_view GetAudioOutParamAttr(OrbisAudioOutParamAttr attr) {
|
||||
switch (attr) {
|
||||
case OrbisAudioOutParamAttr::None:
|
||||
return "NONE";
|
||||
case OrbisAudioOutParamAttr::Restricted:
|
||||
return "RESTRICTED";
|
||||
case OrbisAudioOutParamAttr::MixToMain:
|
||||
return "MIX_TO_MAIN";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsFormatFloat(const OrbisAudioOutParamFormat format) {
|
||||
switch (format) {
|
||||
case OrbisAudioOutParamFormat::S16Mono:
|
||||
case OrbisAudioOutParamFormat::S16Stereo:
|
||||
case OrbisAudioOutParamFormat::S16_8CH:
|
||||
case OrbisAudioOutParamFormat::S16_8CH_Std:
|
||||
return false;
|
||||
case OrbisAudioOutParamFormat::FloatMono:
|
||||
case OrbisAudioOutParamFormat::FloatStereo:
|
||||
case OrbisAudioOutParamFormat::Float_8CH:
|
||||
case OrbisAudioOutParamFormat::Float_8CH_Std:
|
||||
return true;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown format");
|
||||
}
|
||||
}
|
||||
|
||||
static u8 GetFormatNumChannels(const OrbisAudioOutParamFormat format) {
|
||||
switch (format) {
|
||||
case OrbisAudioOutParamFormat::S16Mono:
|
||||
case OrbisAudioOutParamFormat::FloatMono:
|
||||
return 1;
|
||||
case OrbisAudioOutParamFormat::S16Stereo:
|
||||
case OrbisAudioOutParamFormat::FloatStereo:
|
||||
return 2;
|
||||
case OrbisAudioOutParamFormat::S16_8CH:
|
||||
case OrbisAudioOutParamFormat::Float_8CH:
|
||||
case OrbisAudioOutParamFormat::S16_8CH_Std:
|
||||
case OrbisAudioOutParamFormat::Float_8CH_Std:
|
||||
return 8;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown format");
|
||||
}
|
||||
}
|
||||
|
||||
static u8 GetFormatSampleSize(const OrbisAudioOutParamFormat format) {
|
||||
switch (format) {
|
||||
case OrbisAudioOutParamFormat::S16Mono:
|
||||
case OrbisAudioOutParamFormat::S16Stereo:
|
||||
case OrbisAudioOutParamFormat::S16_8CH:
|
||||
case OrbisAudioOutParamFormat::S16_8CH_Std:
|
||||
return 2;
|
||||
case OrbisAudioOutParamFormat::FloatMono:
|
||||
case OrbisAudioOutParamFormat::FloatStereo:
|
||||
case OrbisAudioOutParamFormat::Float_8CH:
|
||||
case OrbisAudioOutParamFormat::Float_8CH_Std:
|
||||
return 4;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown format");
|
||||
}
|
||||
static AudioFormatInfo GetFormatInfo(const OrbisAudioOutParamFormat format) {
|
||||
static constexpr std::array<AudioFormatInfo, 8> format_infos = {{
|
||||
// S16Mono
|
||||
{false, 2, 1, {0}},
|
||||
// S16Stereo
|
||||
{false, 2, 2, {0, 1}},
|
||||
// S16_8CH
|
||||
{false, 2, 8, {0, 1, 2, 3, 4, 5, 6, 7}},
|
||||
// FloatMono
|
||||
{true, 4, 1, {0}},
|
||||
// FloatStereo
|
||||
{true, 4, 2, {0, 1}},
|
||||
// Float_8CH
|
||||
{true, 4, 8, {0, 1, 2, 3, 4, 5, 6, 7}},
|
||||
// S16_8CH_Std
|
||||
{false, 2, 8, {0, 1, 2, 3, 6, 7, 4, 5}},
|
||||
// Float_8CH_Std
|
||||
{true, 4, 8, {0, 1, 2, 3, 6, 7, 4, 5}},
|
||||
}};
|
||||
const auto index = static_cast<u32>(format);
|
||||
ASSERT_MSG(index < format_infos.size(), "Unknown audio format {}", index);
|
||||
return format_infos[index];
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceAudioOutDeviceIdOpen() {
|
||||
|
@ -180,6 +99,10 @@ int PS4_SYSV_ABI sceAudioOutClose(s32 handle) {
|
|||
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
|
||||
}
|
||||
|
||||
port.output_thread.Stop();
|
||||
std::free(port.output_buffer);
|
||||
port.output_buffer = nullptr;
|
||||
port.output_ready = false;
|
||||
port.impl = nullptr;
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
@ -263,7 +186,7 @@ int PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* sta
|
|||
case OrbisAudioOutPort::Bgm:
|
||||
case OrbisAudioOutPort::Voice:
|
||||
state->output = 1;
|
||||
state->channel = port.channels_num > 2 ? 2 : port.channels_num;
|
||||
state->channel = port.format_info.num_channels > 2 ? 2 : port.format_info.num_channels;
|
||||
break;
|
||||
case OrbisAudioOutPort::Personal:
|
||||
case OrbisAudioOutPort::Padspk:
|
||||
|
@ -311,16 +234,7 @@ 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>();
|
||||
}
|
||||
audio = std::make_unique<SDLAudioOut>();
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
|
@ -354,6 +268,30 @@ int PS4_SYSV_ABI sceAudioOutMbusInit() {
|
|||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
static void AudioOutputThread(PortOut* port, const std::stop_token& stop) {
|
||||
{
|
||||
const auto thread_name = fmt::format("shadPS4:AudioOutputThread:{}", fmt::ptr(port));
|
||||
Common::SetCurrentThreadName(thread_name.c_str());
|
||||
}
|
||||
|
||||
Common::AccurateTimer timer(
|
||||
std::chrono::nanoseconds(1000000000ULL * port->buffer_frames / port->sample_rate));
|
||||
while (true) {
|
||||
timer.Start();
|
||||
{
|
||||
std::unique_lock lock{port->output_mutex};
|
||||
Common::CondvarWait(port->output_cv, lock, stop, [&] { return port->output_ready; });
|
||||
if (stop.stop_requested()) {
|
||||
break;
|
||||
}
|
||||
port->impl->Output(port->output_buffer);
|
||||
port->output_ready = false;
|
||||
}
|
||||
port->output_cv.notify_one();
|
||||
timer.End();
|
||||
}
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
|
||||
OrbisAudioOutPort port_type, s32 index, u32 length,
|
||||
u32 sample_rate,
|
||||
|
@ -361,9 +299,9 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
|
|||
LOG_INFO(Lib_AudioOut,
|
||||
"id = {} port_type = {} index = {} length = {} sample_rate = {} "
|
||||
"param_type = {} attr = {}",
|
||||
user_id, GetAudioOutPort(port_type), index, length, sample_rate,
|
||||
GetAudioOutParamFormat(param_type.data_format),
|
||||
GetAudioOutParamAttr(param_type.attributes));
|
||||
user_id, magic_enum::enum_name(port_type), index, length, sample_rate,
|
||||
magic_enum::enum_name(param_type.data_format.Value()),
|
||||
magic_enum::enum_name(param_type.attributes.Value()));
|
||||
if ((port_type < OrbisAudioOutPort::Main || port_type > OrbisAudioOutPort::Padspk) &&
|
||||
(port_type != OrbisAudioOutPort::Aux)) {
|
||||
LOG_ERROR(Lib_AudioOut, "Invalid port type");
|
||||
|
@ -403,17 +341,18 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
|
|||
}
|
||||
|
||||
port->type = port_type;
|
||||
port->format = format;
|
||||
port->is_float = IsFormatFloat(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->format_info = GetFormatInfo(format);
|
||||
port->sample_rate = sample_rate;
|
||||
port->buffer_frames = length;
|
||||
port->volume.fill(SCE_AUDIO_OUT_VOLUME_0DB);
|
||||
|
||||
port->impl = audio->Open(*port);
|
||||
|
||||
port->output_buffer = std::malloc(port->BufferSize());
|
||||
port->output_ready = false;
|
||||
port->output_thread.Run(
|
||||
[port](const std::stop_token& stop) { AudioOutputThread(&*port, stop); });
|
||||
|
||||
return std::distance(ports_out.begin(), port) + 1;
|
||||
}
|
||||
|
||||
|
@ -426,24 +365,30 @@ 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;
|
||||
}
|
||||
if (ptr == nullptr) {
|
||||
// Nothing to output
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
auto& port = ports_out.at(handle - 1);
|
||||
if (!port.impl) {
|
||||
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
|
||||
}
|
||||
|
||||
port.impl->Output(ptr, port.buffer_size);
|
||||
{
|
||||
std::unique_lock lock{port.output_mutex};
|
||||
port.output_cv.wait(lock, [&] { return !port.output_ready; });
|
||||
if (ptr != nullptr) {
|
||||
std::memcpy(port.output_buffer, ptr, port.BufferSize());
|
||||
port.output_ready = true;
|
||||
}
|
||||
}
|
||||
port.output_cv.notify_one();
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num) {
|
||||
for (u32 i = 0; i < num; i++) {
|
||||
if (const auto err = sceAudioOutOutput(param[i].handle, param[i].ptr); err != 0)
|
||||
return err;
|
||||
const auto [handle, ptr] = param[i];
|
||||
if (const auto ret = sceAudioOutOutput(handle, ptr); ret != ORBIS_OK) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
@ -549,30 +494,9 @@ s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol) {
|
|||
return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT;
|
||||
}
|
||||
|
||||
for (int i = 0; i < port.channels_num; i++, flag >>= 1u) {
|
||||
auto bit = flag & 0x1u;
|
||||
if (bit == 1) {
|
||||
int src_index = i;
|
||||
if (port.format == OrbisAudioOutParamFormat::Float_8CH_Std ||
|
||||
port.format == OrbisAudioOutParamFormat::S16_8CH_Std) {
|
||||
switch (i) {
|
||||
case 4:
|
||||
src_index = 6;
|
||||
break;
|
||||
case 5:
|
||||
src_index = 7;
|
||||
break;
|
||||
case 6:
|
||||
src_index = 4;
|
||||
break;
|
||||
case 7:
|
||||
src_index = 5;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
port.volume[i] = vol[src_index];
|
||||
for (int i = 0; i < port.format_info.num_channels; i++, flag >>= 1u) {
|
||||
if (flag & 0x1u) {
|
||||
port.volume[i] = vol[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <memory>
|
||||
|
||||
#include "common/bit_field.h"
|
||||
#include "core/libraries/kernel/threads.h"
|
||||
#include "core/libraries/system/userservice.h"
|
||||
|
||||
namespace Libraries::AudioOut {
|
||||
|
@ -14,12 +15,12 @@ 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;
|
||||
constexpr int SCE_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value
|
||||
constexpr s32 SCE_AUDIO_OUT_NUM_PORTS = 22;
|
||||
constexpr s32 SCE_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value
|
||||
|
||||
enum class OrbisAudioOutPort { Main = 0, Bgm = 1, Voice = 2, Personal = 3, Padspk = 4, Aux = 127 };
|
||||
|
||||
enum class OrbisAudioOutParamFormat {
|
||||
enum class OrbisAudioOutParamFormat : u32 {
|
||||
S16Mono = 0,
|
||||
S16Stereo = 1,
|
||||
S16_8CH = 2,
|
||||
|
@ -30,7 +31,7 @@ enum class OrbisAudioOutParamFormat {
|
|||
Float_8CH_Std = 7
|
||||
};
|
||||
|
||||
enum class OrbisAudioOutParamAttr {
|
||||
enum class OrbisAudioOutParamAttr : u32 {
|
||||
None = 0,
|
||||
Restricted = 1,
|
||||
MixToMain = 2,
|
||||
|
@ -59,19 +60,37 @@ struct OrbisAudioOutPortState {
|
|||
u64 reserved64[2];
|
||||
};
|
||||
|
||||
struct AudioFormatInfo {
|
||||
bool is_float;
|
||||
u8 sample_size;
|
||||
u8 num_channels;
|
||||
/// Layout array remapping channel indices, specified in this order:
|
||||
/// FL, FR, FC, LFE, BL, BR, SL, SR
|
||||
std::array<int, 8> channel_layout;
|
||||
|
||||
[[nodiscard]] u16 FrameSize() const {
|
||||
return sample_size * num_channels;
|
||||
}
|
||||
};
|
||||
|
||||
struct PortOut {
|
||||
std::unique_ptr<PortBackend> impl{};
|
||||
|
||||
void* output_buffer;
|
||||
std::mutex output_mutex;
|
||||
std::condition_variable_any output_cv;
|
||||
bool output_ready;
|
||||
Kernel::Thread output_thread{};
|
||||
|
||||
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;
|
||||
AudioFormatInfo format_info;
|
||||
u32 sample_rate;
|
||||
u32 buffer_frames;
|
||||
std::array<s32, 8> volume;
|
||||
|
||||
[[nodiscard]] u32 BufferSize() const {
|
||||
return buffer_frames * format_info.FrameSize();
|
||||
}
|
||||
};
|
||||
|
||||
int PS4_SYSV_ABI sceAudioOutDeviceIdOpen();
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
typedef struct cubeb cubeb;
|
||||
|
||||
namespace Libraries::AudioOut {
|
||||
|
||||
struct PortOut;
|
||||
|
@ -13,7 +11,10 @@ class PortBackend {
|
|||
public:
|
||||
virtual ~PortBackend() = default;
|
||||
|
||||
virtual void Output(void* ptr, size_t size) = 0;
|
||||
/// Guaranteed to be called in intervals of at least port buffer time,
|
||||
/// with size equal to port buffer size.
|
||||
virtual void Output(void* ptr) = 0;
|
||||
|
||||
virtual void SetVolume(const std::array<int, 8>& ch_volumes) = 0;
|
||||
};
|
||||
|
||||
|
@ -25,20 +26,6 @@ public:
|
|||
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;
|
||||
#ifdef _WIN32
|
||||
bool owns_com = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
class SDLAudioOut final : public AudioOutBackend {
|
||||
public:
|
||||
std::unique_ptr<PortBackend> Open(PortOut& port) override;
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
// 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/logging/log.h"
|
||||
#include "common/ringbuffer.h"
|
||||
#include "core/libraries/audio/audioout.h"
|
||||
#include "core/libraries/audio/audioout_backend.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Objbase.h>
|
||||
#endif
|
||||
|
||||
namespace Libraries::AudioOut {
|
||||
|
||||
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);
|
||||
cubeb_stream_destroy(stream);
|
||||
stream = nullptr;
|
||||
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 {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
#ifdef _WIN32
|
||||
// Need to initialize COM for this thread on Windows, in case WASAPI backend is used.
|
||||
owns_com = CoInitializeEx(nullptr, COINIT_MULTITHREADED) == S_OK;
|
||||
#endif
|
||||
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) {
|
||||
cubeb_destroy(ctx);
|
||||
ctx = nullptr;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (owns_com) {
|
||||
CoUninitialize();
|
||||
owns_com = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<PortBackend> CubebAudioOut::Open(PortOut& port) {
|
||||
return std::make_unique<CubebPortBackend>(ctx, port);
|
||||
}
|
||||
|
||||
} // namespace Libraries::AudioOut
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include <thread>
|
||||
#include <SDL3/SDL_audio.h>
|
||||
#include <SDL3/SDL_hints.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/libraries/audio/audioout.h"
|
||||
|
@ -10,15 +11,21 @@
|
|||
|
||||
namespace Libraries::AudioOut {
|
||||
|
||||
constexpr int AUDIO_STREAM_BUFFER_THRESHOLD = 65536; // Define constant for buffer threshold
|
||||
|
||||
class SDLPortBackend : public PortBackend {
|
||||
public:
|
||||
explicit SDLPortBackend(const PortOut& port) {
|
||||
explicit SDLPortBackend(const PortOut& port)
|
||||
: frame_size(port.format_info.FrameSize()), buffer_size(port.BufferSize()) {
|
||||
// We want the latency for delivering frames out to be as small as possible,
|
||||
// so set the sample frames hint to the number of frames per buffer.
|
||||
const auto samples_num_str = std::to_string(port.buffer_frames);
|
||||
if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, samples_num_str.c_str())) {
|
||||
LOG_WARNING(Lib_AudioOut, "Failed to set SDL audio sample frames hint to {}: {}",
|
||||
samples_num_str, SDL_GetError());
|
||||
}
|
||||
const SDL_AudioSpec fmt = {
|
||||
.format = port.is_float ? SDL_AUDIO_F32 : SDL_AUDIO_S16,
|
||||
.channels = port.channels_num,
|
||||
.freq = static_cast<int>(port.freq),
|
||||
.format = port.format_info.is_float ? SDL_AUDIO_F32LE : SDL_AUDIO_S16LE,
|
||||
.channels = port.format_info.num_channels,
|
||||
.freq = static_cast<int>(port.sample_rate),
|
||||
};
|
||||
stream =
|
||||
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &fmt, nullptr, nullptr);
|
||||
|
@ -26,6 +33,15 @@ public:
|
|||
LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
queue_threshold = CalculateQueueThreshold();
|
||||
if (!SDL_SetAudioStreamInputChannelMap(stream, port.format_info.channel_layout.data(),
|
||||
port.format_info.num_channels)) {
|
||||
LOG_ERROR(Lib_AudioOut, "Failed to configure SDL audio stream channel map: {}",
|
||||
SDL_GetError());
|
||||
SDL_DestroyAudioStream(stream);
|
||||
stream = nullptr;
|
||||
return;
|
||||
}
|
||||
if (!SDL_ResumeAudioStreamDevice(stream)) {
|
||||
LOG_ERROR(Lib_AudioOut, "Failed to resume SDL audio stream: {}", SDL_GetError());
|
||||
SDL_DestroyAudioStream(stream);
|
||||
|
@ -42,14 +58,23 @@ public:
|
|||
stream = nullptr;
|
||||
}
|
||||
|
||||
void Output(void* ptr, size_t size) override {
|
||||
void Output(void* ptr) override {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
SDL_PutAudioStreamData(stream, ptr, static_cast<int>(size));
|
||||
while (SDL_GetAudioStreamAvailable(stream) > AUDIO_STREAM_BUFFER_THRESHOLD) {
|
||||
// Yield to allow the stream to drain.
|
||||
std::this_thread::yield();
|
||||
// AudioOut library manages timing, but we still need to guard against the SDL
|
||||
// audio queue stalling, which may happen during device changes, for example.
|
||||
// Otherwise, latency may grow over time unbounded.
|
||||
if (const auto queued = SDL_GetAudioStreamQueued(stream); queued >= queue_threshold) {
|
||||
LOG_WARNING(Lib_AudioOut,
|
||||
"SDL audio queue backed up ({} queued, {} threshold), clearing.", queued,
|
||||
queue_threshold);
|
||||
SDL_ClearAudioStream(stream);
|
||||
// Recalculate the threshold in case this happened because of a device change.
|
||||
queue_threshold = CalculateQueueThreshold();
|
||||
}
|
||||
if (!SDL_PutAudioStreamData(stream, ptr, static_cast<int>(buffer_size))) {
|
||||
LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,6 +91,21 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] u32 CalculateQueueThreshold() const {
|
||||
SDL_AudioSpec discard;
|
||||
int sdl_buffer_frames;
|
||||
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard,
|
||||
&sdl_buffer_frames)) {
|
||||
LOG_WARNING(Lib_AudioOut, "Failed to get SDL audio stream buffer size: {}",
|
||||
SDL_GetError());
|
||||
sdl_buffer_frames = 0;
|
||||
}
|
||||
return std::max<u32>(buffer_size, sdl_buffer_frames * frame_size) * 4;
|
||||
}
|
||||
|
||||
u32 frame_size;
|
||||
u32 buffer_size;
|
||||
u32 queue_threshold;
|
||||
SDL_AudioStream* stream;
|
||||
};
|
||||
|
||||
|
|
|
@ -211,7 +211,6 @@ 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);
|
||||
|
@ -305,8 +304,6 @@ 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));
|
||||
|
@ -428,8 +425,6 @@ void SettingsDialog::updateNoteTextEdit(const QString& elementName) {
|
|||
text = tr("checkCompatibilityOnStartupCheckBox");
|
||||
} else if (elementName == "updateCompatibilityButton") {
|
||||
text = tr("updateCompatibilityButton");
|
||||
} else if (elementName == "audioBackendGroupBox") {
|
||||
text = tr("audioBackendGroupBox");
|
||||
}
|
||||
|
||||
// Input
|
||||
|
@ -543,7 +538,6 @@ 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();
|
||||
|
|
|
@ -263,29 +263,6 @@
|
|||
</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>
|
||||
|
|
Loading…
Reference in a new issue